@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.7

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 (93) hide show
  1. package/CHANGELOG.en.md +44 -1
  2. package/CHANGELOG.md +44 -1
  3. package/README.en.md +155 -92
  4. package/README.md +156 -91
  5. package/cordis.patch.yml +57 -0
  6. package/lib/client.js +2678 -276
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +7 -2
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-Kup4_ZIb.js +747 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +229 -20
  28. package/lib/types/client/RootSwitcher.d.ts +63 -0
  29. package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
  30. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  31. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  32. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  33. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  34. package/lib/types/client/files-tree/face.d.ts +107 -0
  35. package/lib/types/client/files-tree/locales.d.ts +10 -0
  36. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  37. package/lib/types/client/files-tree/store.d.ts +81 -0
  38. package/lib/types/client/locales.d.ts +56 -10
  39. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  40. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  41. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  42. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  43. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  44. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  45. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  46. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  47. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  48. package/lib/types/client/panel-client.d.ts +2 -1
  49. package/lib/types/client/styles.d.ts +1 -1
  50. package/lib/types/command.d.ts +16 -3
  51. package/lib/types/compat/client-icons.d.ts +18 -0
  52. package/lib/types/compat/dsh-version.d.ts +1 -1
  53. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  54. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  55. package/lib/types/compat/process-result.d.ts +14 -0
  56. package/lib/types/compat/pty-marker.d.ts +83 -0
  57. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  58. package/lib/types/compat/storage-zod.d.ts +65 -0
  59. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  60. package/lib/types/compat/windows-native.d.ts +68 -0
  61. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  62. package/lib/types/compat/workspace-files.d.ts +2 -0
  63. package/lib/types/contract.d.ts +31 -19
  64. package/lib/types/dialects.d.ts +99 -3
  65. package/lib/types/fs.d.ts +32 -10
  66. package/lib/types/index.d.ts +7 -2
  67. package/lib/types/process-fence.d.ts +54 -0
  68. package/lib/types/profile-isolation.d.ts +178 -0
  69. package/lib/types/providers.d.ts +70 -0
  70. package/lib/types/registry.d.ts +132 -31
  71. package/lib/types/roots.d.ts +100 -13
  72. package/lib/types/sandbox.d.ts +59 -3
  73. package/lib/types/scope.d.ts +200 -14
  74. package/lib/types/windows-broker.d.ts +17 -0
  75. package/lib/types/windows-deny.d.ts +1 -0
  76. package/lib/types/windows-profile.d.ts +10 -13
  77. package/lib/types/windows-runner-contract.d.ts +97 -0
  78. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  79. package/lib/windows-runner.js +1256 -13
  80. package/lib/workspace-files.js +138 -3
  81. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  82. package/package.json +84 -62
  83. package/scripts/windows-broker/install.ps1 +89 -0
  84. package/scripts/windows-broker/rights.cs +58 -0
  85. package/scripts/windows-broker/service.cs +323 -0
  86. package/scripts/windows-broker/verify-install.ps1 +80 -0
  87. package/lib/command-CTdNvwt1.js +0 -791
  88. package/lib/method-wrapper-Zc-JlDP3.js +0 -51
  89. package/lib/sandbox-BRXl_FpF.js +0 -132
  90. package/lib/types/client/RootFileTree.d.ts +0 -12
  91. package/lib/types/panel-files.d.ts +0 -3
  92. package/lib/windows-profile-CiFsFYIV.js +0 -216
  93. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
@@ -1,34 +1,1277 @@
1
- import { r as workspaceScopeSid, t as parseWindowsProfile } from "./windows-profile-CiFsFYIV.js";
2
- import { join } from "node:path";
3
- import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
4
- import { closeSync, mkdtempSync, rmSync, statSync } from "node:fs";
1
+ import { a as parseWindowsProfile, i as isWindowsNetworkOrDevicePath, n as assertWindowsDenyCapacity, o as rootDenySid, r as canonicalPath, s as workspaceScopeSid } from "./windows-runner-contract-CEfmZzu0.js";
2
+ import { createRequire } from "node:module";
3
+ import { closeSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join, win32 } from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ import { pathToFileURL } from "node:url";
7
+ import { createConnection } from "node:net";
8
+ //#region src/compat/windows-native.ts
9
+ /** Public Win32 process primitives, resolved from the upstream ACL installation.
10
+ * The standalone runner has no host module resolver. No upstream private file
11
+ * or load-time optional dependency is used here; unfamiliar exports fail closed.
12
+ */
13
+ /** Decode the UTF-16 pointee, not a second string pointer stored at its first
14
+ * code units. `decode(ptr, 'str16')` would dereference those units as an address.
15
+ */
16
+ function readWindowsWideString(koffi, pointer) {
17
+ return String(koffi.decode(pointer, "char16_t", -1));
18
+ }
19
+ async function loadWindowsNative(upstreamRunner) {
20
+ const aclEntry = createRequire(upstreamRunner).resolve("@deepseek-ai/dsh-sandbox-windows-acl");
21
+ const fromAcl = createRequire(aclEntry);
22
+ const processApi = await import(pathToFileURL(fromAcl.resolve("@deepseek-ai/dsh-win32-process")).href);
23
+ const koffi = (await import(pathToFileURL(fromAcl.resolve("koffi")).href)).default;
24
+ for (const name of [
25
+ "extendWin32ProcessBindings",
26
+ "spawnInheritedJobProcess",
27
+ "waitForProcessExit"
28
+ ]) if (typeof processApi[name] !== "function") throw new Error(`Windows ambient restriction requires public Win32 export ${name}`);
29
+ for (const name of [
30
+ "decode",
31
+ "address",
32
+ "load",
33
+ "alloc",
34
+ "free",
35
+ "encode"
36
+ ]) if (typeof koffi?.[name] !== "function") throw new Error(`Windows ambient restriction requires Koffi ${name}`);
37
+ return {
38
+ ...processApi,
39
+ koffi
40
+ };
41
+ }
42
+ /** Older public process helpers predate fd-7 CRT forwarding. Adapt the Win32
43
+ * STARTUPINFO structure at the public binding seam, preserving argv and Job
44
+ * ownership. New helpers already populate cbReserved2 and pass through.
45
+ */
46
+ function spawnWindowsDeniedChild(native, api, options, activation) {
47
+ if (options.controlFileDescriptor !== void 0 && typeof api.getFileType !== "function") api = {
48
+ ...api,
49
+ ...native.extendWin32ProcessBindings(({ kernel32, bind }) => ({ getFileType: bind(kernel32, "GetFileType", "uint32", ["void*"]) }))
50
+ };
51
+ let launchApi = api;
52
+ if (activation !== void 0) launchApi = {
53
+ ...api,
54
+ createProcessAsUserW: (...args) => {
55
+ if ((Number(args[6]) & 4) === 0) throw new Error("Windows deny activation requires a suspended child");
56
+ const result = api.createProcessAsUserW(activation.sourceToken, ...args.slice(1));
57
+ if (result === 0) return result;
58
+ const processInfo = Buffer.from(native.koffi.decode(args[10], "uint8", 24));
59
+ const process = native.koffi.decode(processInfo, "void*");
60
+ const thread = native.koffi.decode(processInfo.subarray(8), "void*");
61
+ try {
62
+ activation.assign(process, thread);
63
+ } catch (error) {
64
+ api.terminateProcess(process, 127);
65
+ api.closeHandle(thread);
66
+ api.closeHandle(process);
67
+ throw error;
68
+ }
69
+ return result;
70
+ }
71
+ };
72
+ if (options.controlFileDescriptor === void 0) return native.spawnInheritedJobProcess(launchApi, options);
73
+ const handle = api.uvGetOsfhandle(7);
74
+ if (api.getFileType(handle) !== 3) throw new Error("subprocess control descriptor is not a Windows pipe");
75
+ if (api.setHandleInformation(handle, 1, 1) === 0) throw new Error(`SetHandleInformation(control) failed (Win32 ${api.getLastError()})`);
76
+ const adapted = {
77
+ ...launchApi,
78
+ createProcessAsUserW: (...args) => {
79
+ const original = Buffer.from(native.koffi.decode(args[9], "uint8", 104));
80
+ if (original.readUInt32LE(0) !== 104) throw new Error("unrecognized Windows STARTUPINFO layout");
81
+ if (original.readUInt16LE(66) !== 0) return launchApi.createProcessAsUserW(...args);
82
+ const count = 8;
83
+ const data = Buffer.alloc(76);
84
+ data.writeUInt32LE(count);
85
+ for (let fd = 0; fd < count; fd++) data.writeBigUInt64LE(18446744073709551615n, 12 + fd * 8);
86
+ for (const [fd, value] of [
87
+ [0, original.readBigUInt64LE(80)],
88
+ [1, original.readBigUInt64LE(88)],
89
+ [2, original.readBigUInt64LE(96)],
90
+ [7, handle]
91
+ ]) {
92
+ const kind = api.getFileType(value);
93
+ data[4 + fd] = 1 | (kind === 3 ? 8 : kind === 2 ? 64 : 0);
94
+ data.writeBigUInt64LE(BigInt(value), 12 + fd * 8);
95
+ }
96
+ const reserved = native.koffi.alloc("uint8", data.length);
97
+ try {
98
+ native.koffi.encode(reserved, "uint8", data, data.length);
99
+ original.writeUInt16LE(data.length, 66);
100
+ original.writeBigUInt64LE(native.koffi.address(reserved), 72);
101
+ return launchApi.createProcessAsUserW(...args.slice(0, 9), original, ...args.slice(10));
102
+ } finally {
103
+ native.koffi.free(reserved);
104
+ }
105
+ }
106
+ };
107
+ try {
108
+ return native.spawnInheritedJobProcess(adapted, options);
109
+ } finally {
110
+ api.setHandleInformation(handle, 1, 0);
111
+ }
112
+ }
113
+ /**
114
+ * Terminate a kill-on-close Job and wait until the kernel reports it drained.
115
+ *
116
+ * The public `waitForProcessExit` covers the direct child only: a descendant
117
+ * the child spawned before exiting stays in the Job and could otherwise keep
118
+ * running — with its restricted token — after the runner has released its
119
+ * ancestor pins. `TerminateJobObject(job, 127)` kills every member; the
120
+ * subsequent `QueryInformationJobObject` polling loop (class 1,
121
+ * JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, `ActiveProcesses` at byte 40) only
122
+ * returns once the kernel reports none left. There is deliberately no
123
+ * timeout: releasing pins while a live descendant remains is exactly the gap
124
+ * this wait exists to close. The caller still owns and closes the Job handle
125
+ * afterwards.
126
+ *
127
+ * Abnormal-error boundary: a TerminateJobObject or QueryInformationJobObject
128
+ * API failure throws into the caller's teardown — the Job handle is still
129
+ * closed there and its kill-on-close flag is the residual bound, same as the
130
+ * forced-termination case: if the runner process itself is killed, no
131
+ * in-process guarantee can survive, the Job flag takes the descendants down
132
+ * with it, and the pin handles die with the process. Protection is promised
133
+ * only through this orderly path.
134
+ */
135
+ function terminateWindowsJobAndWait(native, api, job) {
136
+ const jobApi = {
137
+ ...api,
138
+ ...native.extendWin32ProcessBindings(({ kernel32, bind }) => ({
139
+ terminateJobObject: bind(kernel32, "TerminateJobObject", "int", ["void*", "uint32"]),
140
+ queryInformationJobObject: bind(kernel32, "QueryInformationJobObject", "int", [
141
+ "void*",
142
+ "int",
143
+ "void*",
144
+ "uint32",
145
+ "void*"
146
+ ]),
147
+ sleep: bind(kernel32, "Sleep", "void", ["uint32"]),
148
+ getLastError: bind(kernel32, "GetLastError", "uint32", [])
149
+ }))
150
+ };
151
+ if (jobApi.terminateJobObject(job, 127) === 0) throw new Error(`TerminateJobObject failed (Win32 ${jobApi.getLastError()})`);
152
+ const info = Buffer.alloc(48);
153
+ for (;;) {
154
+ if (jobApi.queryInformationJobObject(job, 1, info, info.length, null) === 0) throw new Error(`QueryInformationJobObject failed (Win32 ${jobApi.getLastError()})`);
155
+ if (info.readUInt32LE(40) === 0) return;
156
+ jobApi.sleep(10);
157
+ }
158
+ }
159
+ //#endregion
160
+ //#region src/compat/windows-path-guard.ts
161
+ /** Windows deny-path pinning (branch-review Phase B).
162
+ *
163
+ * The ACL writer's deny ACEs follow the file OBJECT, not the registered
164
+ * spelling: a restricted process that renames a deny root's ancestor moves
165
+ * the protected tree to a NEW path the recorded deny set does not name — the
166
+ * kernel still refuses that token's writes through the moved ACEs, but the
167
+ * host fs fence, which matches the recorded spellings, no longer covers the
168
+ * moved objects (the shell → host new-path gap, R1). This guard pins every
169
+ * ancestor directory of every deny root with an open handle that shares read
170
+ * and write but NOT delete — so a rename or delete of the registered tree
171
+ * fails for the lifetime of the restricted child, without writing a single
172
+ * persistent ACL on `C:\`, user directories, or anywhere else.
173
+ *
174
+ * `windows-deny.ts` owns the lifetime contract: handles are held until the
175
+ * confined child AND its drained Job descendants have exited.
176
+ *
177
+ * Primitives are bound through the public `extendWin32ProcessBindings` seam —
178
+ * no upstream private file, no new native dependency. Every verification uses
179
+ * the same handle that pins the directory, so the checked object and the
180
+ * pinned object are always the same one.
181
+ */
182
+ /** The access an ancestor pin needs to PARTICIPATE in the delete-sharing
183
+ * gate. Verified on Windows 11: a metadata-only open (FILE_READ_ATTRIBUTES
184
+ * alone, even with share mode 0) is transparent to the gate — it neither
185
+ * blocks a rename of the directory nor is blocked by a conflicting handle —
186
+ * while verified data-read access (GENERIC_READ here) does block it: the pin
187
+ * then holds renames while still sharing read and write.
188
+ * A consequence, not a bug: an ancestor the caller cannot READ cannot be
189
+ * pinned, and the pin fails closed instead of pretending. */
190
+ const PIN_ACCESS = 2147483648;
191
+ /** READ|WRITE sharing, deliberately WITHOUT FILE_SHARE_DELETE: that is the
192
+ * whole mechanism — a rename or delete of a pinned directory needs delete
193
+ * sharing on it, which nothing gets while a pin is held. */
194
+ const SHARE_READ_WRITE = 3;
195
+ const OPEN_EXISTING$1 = 3;
196
+ /** Directory handles need BACKUP_SEMANTICS; OPEN_REPARSE_POINT makes the pin
197
+ * land on the reparse point itself so the attribute check can refuse it. */
198
+ const PIN_FLAGS = 35651584;
199
+ const FILE_ATTRIBUTE_DIRECTORY$1 = 16;
200
+ const FILE_ATTRIBUTE_REPARSE_POINT = 1024;
201
+ const HANDLE_FLAG_INHERIT = 1;
202
+ /** `\\?\` extended-length prefix on a normalized DOS volume name; a `UNC`
203
+ * continuation means the handle resolved onto a network share. */
204
+ const DOS_PREFIX = "\\\\?\\";
205
+ /** Bind the file/handle primitives the guard needs through the public seam. */
206
+ function pathGuardBindings(native) {
207
+ return native.extendWin32ProcessBindings(({ kernel32, bind }) => ({
208
+ createFileW: bind(kernel32, "CreateFileW", "void*", [
209
+ "str16",
210
+ "uint32",
211
+ "uint32",
212
+ "void*",
213
+ "uint32",
214
+ "uint32",
215
+ "void*"
216
+ ]),
217
+ getFileInformationByHandle: bind(kernel32, "GetFileInformationByHandle", "int", ["void*", "void*"]),
218
+ getFinalPathNameByHandleW: bind(kernel32, "GetFinalPathNameByHandleW", "uint32", [
219
+ "void*",
220
+ "void*",
221
+ "uint32",
222
+ "uint32"
223
+ ]),
224
+ getHandleInformation: bind(kernel32, "GetHandleInformation", "int", ["void*", "void*"]),
225
+ closeHandle: bind(kernel32, "CloseHandle", "int", ["void*"]),
226
+ getLastError: bind(kernel32, "GetLastError", "uint32", [])
227
+ }));
228
+ }
229
+ const isInvalidHandle = (handle) => handle === null || handle === void 0 || BigInt(handle) === -1n || BigInt(handle) === 0n || BigInt(handle) === 18446744073709551615n;
230
+ /** BY_HANDLE_FILE_INFORMATION is 52 bytes (FILETIME members are DWORD-pairs):
231
+ * attributes @0, volume serial @28, link count @40, file index high @44 /
232
+ * low @48. Read back on the same handle that pins the path, so the identity
233
+ * cannot drift between check and pin.
234
+ */
235
+ function readWindowsPathIdentity(api, handle) {
236
+ const info = Buffer.alloc(52);
237
+ if (api.getFileInformationByHandle(handle, info) === 0) throw new Error(`GetFileInformationByHandle failed (Win32 ${api.getLastError()})`);
238
+ return {
239
+ attributes: info.readUInt32LE(0),
240
+ volumeSerial: info.readUInt32LE(28),
241
+ links: info.readUInt32LE(40),
242
+ fileIndex: BigInt(info.readUInt32LE(44)) << 32n | BigInt(info.readUInt32LE(48))
243
+ };
244
+ }
245
+ /**
246
+ * The kernel's normalized DOS name for an open handle, compared against the
247
+ * spelling the caller expected. Anything else — a junction-resolved target, a
248
+ * UNC share, a device path — is a refusal, never an assumption.
249
+ */
250
+ function readFinalPathName(api, handle, expected) {
251
+ let capacity = 260;
252
+ for (;;) {
253
+ const buffer = Buffer.alloc(capacity * 2);
254
+ const written = api.getFinalPathNameByHandleW(handle, buffer, capacity, 0);
255
+ if (written === 0) throw new Error(`GetFinalPathNameByHandleW(${expected}) failed (Win32 ${api.getLastError()})`);
256
+ if (written >= capacity) {
257
+ capacity = written + 1;
258
+ continue;
259
+ }
260
+ const name = buffer.subarray(0, written * 2).toString("utf16le");
261
+ const finalPath = name.startsWith(DOS_PREFIX) ? name.slice(4) : name;
262
+ if (finalPath.startsWith("UNC\\") || finalPath.startsWith("\\\\")) throw new Error(`deny path ancestor resolves to a non-local device: ${expected}`);
263
+ if (finalPath.toLowerCase() !== expected.toLowerCase()) throw new Error(`deny path ancestor resolves to "${finalPath}" instead of "${expected}"`);
264
+ return;
265
+ }
266
+ }
267
+ /**
268
+ * Every directory that must not move for `roots` to keep their registered
269
+ * meaning: the full chain from the drive root down to each deny root,
270
+ * deduplicated and ordered shallow-first so a shared prefix is pinned once
271
+ * and parents are held before their children.
272
+ *
273
+ * Roots must be canonical absolute local-drive spellings: relative input,
274
+ * UNC, and device paths are rejected lexically before any filesystem touch.
275
+ */
276
+ function denyPathChain(roots) {
277
+ const seen = /* @__PURE__ */ new Map();
278
+ for (const root of roots) {
279
+ const normalized = win32.normalize(root);
280
+ if (!/^[a-zA-Z]:[\\/]/.test(normalized) || isWindowsNetworkOrDevicePath(normalized)) throw new Error(`deny root is not a canonical local-drive path: ${root}`);
281
+ let cursor = normalized.length > 3 ? normalized.replace(/[\\/]+$/, "") : normalized;
282
+ for (;;) {
283
+ if (!seen.has(cursor.toLowerCase())) seen.set(cursor.toLowerCase(), cursor);
284
+ const parent = win32.dirname(cursor);
285
+ if (parent === cursor) break;
286
+ cursor = parent;
287
+ }
288
+ }
289
+ const depth = (path) => path.split("\\").length - 1;
290
+ return [...seen.values()].sort((a, b) => depth(a) - depth(b) || a.localeCompare(b));
291
+ }
292
+ /** Close every handle, deepest first; collect rather than lose failures. */
293
+ function closeAll(api, handles) {
294
+ const errors = [];
295
+ for (let index = handles.length - 1; index >= 0; index -= 1) try {
296
+ if (api.closeHandle(handles[index]) === 0) errors.push(/* @__PURE__ */ new Error(`CloseHandle failed (Win32 ${api.getLastError()})`));
297
+ } catch (error) {
298
+ errors.push(error instanceof Error ? error : new Error(String(error)));
299
+ }
300
+ return errors;
301
+ }
302
+ /**
303
+ * Open a pin handle on every ancestor of every deny root — including the deny
304
+ * root itself — and hold them all until {@link WindowsPathGuard.close}. Each
305
+ * handle is opened with GENERIC_READ, read+write sharing, and a null security
306
+ * descriptor so it is non-inheritable; the missing DELETE sharing is what
307
+ * blocks renames, while ordinary reads and writes (and creating sibling files
308
+ * inside a pinned directory) keep working.
309
+ *
310
+ * Every pin is verified on the same handle: the object must be a directory,
311
+ * must not be a reparse point, must not carry the inherit flag (so a confined
312
+ * child cannot simply `CloseHandle` the pin away), and its kernel-reported
313
+ * final DOS path must equal the expected canonical spelling.
314
+ * Any failure — open, verification, or a sharing conflict with a handle some
315
+ * other component already holds — closes every earlier pin in reverse order
316
+ * and refuses, keeping the launch fail-closed before spawn.
317
+ */
318
+ function pinWindowsDenyPaths(native, roots) {
319
+ const api = pathGuardBindings(native);
320
+ const chain = denyPathChain(roots);
321
+ const handles = [];
322
+ try {
323
+ for (const path of chain) {
324
+ const handle = api.createFileW(path, PIN_ACCESS, SHARE_READ_WRITE, null, OPEN_EXISTING$1, PIN_FLAGS, null);
325
+ if (isInvalidHandle(handle)) throw new Error(`CreateFileW(${path}) failed (Win32 ${api.getLastError()})`);
326
+ handles.push(handle);
327
+ const identity = readWindowsPathIdentity(api, handle);
328
+ if ((identity.attributes & FILE_ATTRIBUTE_REPARSE_POINT) !== 0) throw new Error(`deny path ancestor is a reparse point: ${path}`);
329
+ if ((identity.attributes & FILE_ATTRIBUTE_DIRECTORY$1) === 0) throw new Error(`deny path ancestor is not a directory: ${path}`);
330
+ const flags = Buffer.alloc(4);
331
+ if (api.getHandleInformation(handle, flags) === 0) throw new Error(`GetHandleInformation(${path}) failed (Win32 ${api.getLastError()})`);
332
+ if ((flags.readUInt32LE() & HANDLE_FLAG_INHERIT) !== 0) throw new Error(`deny path handle is inheritable: ${path}`);
333
+ readFinalPathName(api, handle, path);
334
+ }
335
+ } catch (primary) {
336
+ const cleanup = closeAll(api, handles);
337
+ if (cleanup.length > 0) throw new AggregateError([primary instanceof Error ? primary : new Error(String(primary)), ...cleanup], primary instanceof Error ? primary.message : String(primary));
338
+ throw primary;
339
+ }
340
+ let closed = false;
341
+ return { close() {
342
+ if (closed) return;
343
+ closed = true;
344
+ const errors = closeAll(api, handles);
345
+ if (errors.length > 0) throw new AggregateError(errors, "WindowsPathGuard close failed");
346
+ } };
347
+ }
348
+ //#endregion
349
+ //#region src/compat/windows-deny-preflight.ts
350
+ /** Windows deny-tree preflight (branch-review Phase C1, R4).
351
+ *
352
+ * A deny ACE written through `SetSecurityInfo` lands on the file OBJECT — for
353
+ * a hardlinked file every other link carries the same object, so a partial
354
+ * ACL walk would have already mutated paths outside the denied tree by the
355
+ * time a later hardlink is discovered. This module therefore scans EVERY deny
356
+ * root and descendant — read-only, no ACL edits anywhere — before the runner
357
+ * is allowed to request a token or touch a security descriptor. Any
358
+ * uninspectable node or any plain file with more than one link refuses the
359
+ * whole startup (exit 127 before side effects).
360
+ *
361
+ * The policy is conservative: `links > 1` fails even when every link appears
362
+ * to live inside the same scanned tree, because the snapshot cannot prove
363
+ * that topology (a link may be added or moved between scan and mutation).
364
+ * Reparse points are recorded but never descended — a junction's target is a
365
+ * different tree with its own policy and must not be scanned or mutated.
366
+ *
367
+ * `verifyHandle` re-checks identity on the same live handle about to be
368
+ * ACL-mutated: a path replaced between scan and mutation fails closed rather
369
+ * than writing a deny onto an object the preflight never inspected. This is
370
+ * not transactional — a malicious concurrent mutation can still race between
371
+ * the recheck and the write — but it closes the far cheaper
372
+ * replace-between-scan-and-write window.
373
+ *
374
+ * Primitives are bound through the public `extendWin32ProcessBindings` seam;
375
+ * only Node built-ins and local contract/native modules are imported.
376
+ */
377
+ const FILE_READ_ATTRIBUTES = 128;
378
+ const INSPECTION_SHARE = 7;
379
+ const OPEN_EXISTING = 3;
380
+ const OPEN_FLAGS = 35651584;
381
+ const FILE_ATTRIBUTE_DIRECTORY = 16;
382
+ const KIND_BITS = 1040;
383
+ const FILE_TYPE_DISK = 1;
384
+ const INVALID_HANDLE = BigInt("0xFFFFFFFFFFFFFFFF");
385
+ const invalid = (handle) => handle === null || handle === void 0 || BigInt(handle) === INVALID_HANDLE || BigInt(handle) === -1n;
386
+ const kindOf = (root, protectedHome) => protectedHome !== void 0 && root.toLowerCase() === protectedHome.toLowerCase() ? "protected DSH home" : "registered root";
387
+ const hardlinkError = (root, path, links, protectedHome) => {
388
+ const kind = kindOf(root, protectedHome);
389
+ return /* @__PURE__ */ new Error(`hardlinked file inside the ${kind} ${root}: ${path} has links=${links}; a deny ACL written on one link mutates every link's shared object — ${kind === "protected DSH home" ? "replace the file with an independent copy — run `/workspace-folders isolate-profiles` to isolate the protected home, then reinstall the profile dependencies (do not skip this file)" : "replace the file with an independent copy (write its contents to a new file and rename that over it, or reinstall the dependency tree as copies)"}`);
390
+ };
391
+ /**
392
+ * Scan every root recursively. The returned snapshot keys every node by its
393
+ * lowercased walked spelling; a later same-key path (shared subtree) keeps
394
+ * the first entry and is not re-scanned.
395
+ */
396
+ function preflightWindowsDenyRoots(native, roots, protectedHome) {
397
+ const api = native.extendWin32ProcessBindings(({ kernel32, bind }) => ({
398
+ createFileW: bind(kernel32, "CreateFileW", "void*", [
399
+ "str16",
400
+ "uint32",
401
+ "uint32",
402
+ "void*",
403
+ "uint32",
404
+ "uint32",
405
+ "void*"
406
+ ]),
407
+ getFileInformationByHandle: bind(kernel32, "GetFileInformationByHandle", "int", ["void*", "void*"]),
408
+ getFileType: bind(kernel32, "GetFileType", "uint32", ["void*"]),
409
+ closeHandle: bind(kernel32, "CloseHandle", "int", ["void*"]),
410
+ getLastError: bind(kernel32, "GetLastError", "uint32", [])
411
+ }));
412
+ const entries = /* @__PURE__ */ new Map();
413
+ /**
414
+ * Open, identify, record, then close — the handle never outlives the node.
415
+ * Every failure names the root and the path; a close failure is reported on
416
+ * its own when it is the only error and aggregated (never masking) when the
417
+ * inspection itself already failed.
418
+ */
419
+ const inspect = (root, path) => {
420
+ const handle = api.createFileW(path, FILE_READ_ATTRIBUTES, INSPECTION_SHARE, 0, OPEN_EXISTING, OPEN_FLAGS, 0);
421
+ if (invalid(handle)) throw new Error(`deny preflight cannot inspect ${path} under root ${root}: CreateFileW Win32 ${api.getLastError()}`);
422
+ let primary;
423
+ let identity;
424
+ try {
425
+ identity = readWindowsPathIdentity(api, handle);
426
+ if ((identity.attributes & KIND_BITS) === 0) {
427
+ if (api.getFileType(handle) !== FILE_TYPE_DISK) throw new Error(`deny preflight refuses non-disk file ${path} under root ${root}`);
428
+ if (identity.links > 1) throw hardlinkError(root, path, identity.links, protectedHome);
429
+ }
430
+ } catch (error) {
431
+ primary = error instanceof Error ? error : new Error(String(error));
432
+ }
433
+ const closed = api.closeHandle(handle) !== 0;
434
+ if (primary !== void 0) {
435
+ if (!closed) throw new AggregateError([primary, /* @__PURE__ */ new Error(`CloseHandle failed on the inspection handle: Win32 ${api.getLastError()}`)], `deny preflight failed for ${path} under root ${root} and its cleanup also failed`);
436
+ throw primary;
437
+ }
438
+ if (!closed) throw new Error(`deny preflight cannot close the inspection handle for ${path} under root ${root}: Win32 ${api.getLastError()}`);
439
+ return identity;
440
+ };
441
+ const scanNode = (root, path) => {
442
+ const key = path.toLowerCase();
443
+ if (entries.has(key)) return;
444
+ const identity = inspect(root, path);
445
+ entries.set(key, {
446
+ root,
447
+ path,
448
+ identity
449
+ });
450
+ if ((identity.attributes & KIND_BITS) !== FILE_ATTRIBUTE_DIRECTORY) return;
451
+ const canonical = canonicalPath(path);
452
+ if (win32.normalize(canonical).toLowerCase() !== key) throw new Error(`deny preflight refuses aliased directory ${path} under root ${root}: resolves to ${canonical}`);
453
+ let names;
454
+ try {
455
+ names = readdirSync(path).sort();
456
+ } catch (error) {
457
+ throw new Error(`deny preflight cannot list ${path} under root ${root}: ${error instanceof Error ? error.message : String(error)}`);
458
+ }
459
+ for (const name of names) scanNode(root, win32.join(path, name));
460
+ };
461
+ for (const root of roots) {
462
+ const canonicalRoot = win32.normalize(canonicalPath(root));
463
+ const identity = inspect(canonicalRoot, canonicalRoot);
464
+ if ((identity.attributes & KIND_BITS) !== FILE_ATTRIBUTE_DIRECTORY) throw new Error(`deny preflight refuses root ${canonicalRoot}: it must be a plain directory`);
465
+ entries.set(canonicalRoot.toLowerCase(), {
466
+ root: canonicalRoot,
467
+ path: canonicalRoot,
468
+ identity
469
+ });
470
+ let names;
471
+ try {
472
+ names = readdirSync(canonicalRoot).sort();
473
+ } catch (error) {
474
+ throw new Error(`deny preflight cannot list root ${canonicalRoot}: ${error instanceof Error ? error.message : String(error)}`);
475
+ }
476
+ for (const name of names) scanNode(canonicalRoot, win32.join(canonicalRoot, name));
477
+ }
478
+ return {
479
+ entries,
480
+ verifyHandle(liveApi, path, handle) {
481
+ const entry = entries.get(path.toLowerCase());
482
+ if (entry === void 0) throw new Error(`deny preflight has no snapshot for ${path} — refusing to mutate an uninspected path`);
483
+ let fresh;
484
+ try {
485
+ fresh = readWindowsPathIdentity(liveApi, handle);
486
+ } catch (error) {
487
+ throw new Error(`deny preflight cannot re-verify ${path}: ${error instanceof Error ? error.message : String(error)}`);
488
+ }
489
+ if ((fresh.attributes & KIND_BITS) !== (entry.identity.attributes & KIND_BITS)) throw new Error(`deny preflight refuses ${entry.path}: attributes changed since the scan (was 0x${entry.identity.attributes.toString(16)}, now 0x${fresh.attributes.toString(16)})`);
490
+ if (fresh.volumeSerial !== entry.identity.volumeSerial || fresh.fileIndex !== entry.identity.fileIndex) throw new Error(`deny preflight refuses ${entry.path}: the path was replaced by a different file object since the scan`);
491
+ if ((entry.identity.attributes & KIND_BITS) === 0 && fresh.links !== 1) throw hardlinkError(entry.root, entry.path, fresh.links, protectedHome);
492
+ }
493
+ };
494
+ }
495
+ //#endregion
496
+ //#region src/windows-broker.ts
497
+ /** Client of the administrator-installed deny-token broker. Requests carry
498
+ * only the caller process token handle and bounded deny identities.
499
+ */
500
+ function brokerPipeName(userSid) {
501
+ if (!/^S-1-[0-9]+(?:-[0-9]+)+$/.test(userSid)) throw new Error("invalid broker user SID");
502
+ return `DshMultiRootBroker-${createHash("sha256").update(userSid).digest("hex").slice(0, 16)}`;
503
+ }
504
+ /** The broker must acknowledge before any ACL edits or process creation. The
505
+ * caller independently reads the deny-only groups from the token;
506
+ * even an impersonated pipe server cannot fabricate that kernel state.
507
+ */
508
+ async function requestDenyToken(userSid, tokenHandle, deniedIds) {
509
+ if (tokenHandle <= 0n || tokenHandle > 18446744073709551615n || deniedIds.length === 0 || deniedIds.length > 256 || deniedIds.some((id) => !/^S-1-4-(?:[0-9]{1,10}-){4}3$/.test(id))) throw new Error("invalid deny-token broker request");
510
+ const pipe = `\\\\.\\pipe\\${brokerPipeName(userSid)}`;
511
+ return await new Promise((resolve, reject) => {
512
+ const socket = createConnection(pipe);
513
+ let response = "";
514
+ const timer = setTimeout(() => finish(/* @__PURE__ */ new Error("deny-token broker timed out")), 1e4);
515
+ const finish = (error, handle) => {
516
+ clearTimeout(timer);
517
+ socket.destroy();
518
+ if (error === void 0 && handle !== void 0) resolve(handle);
519
+ else reject(error ?? /* @__PURE__ */ new Error("missing broker token handle"));
520
+ };
521
+ socket.once("connect", () => {
522
+ socket.write(`2|${tokenHandle}|${deniedIds.join(",")}\n`);
523
+ });
524
+ socket.once("error", (error) => finish(/* @__PURE__ */ new Error(`Windows read-only roots require the installed privileged broker (${brokerPipeName(userSid)}): ${error.message}`)));
525
+ socket.once("end", () => {
526
+ if (!response.includes("\n")) finish(/* @__PURE__ */ new Error("deny-token broker disconnected"));
527
+ });
528
+ socket.on("data", (bytes) => {
529
+ response += bytes.toString("utf8");
530
+ if (response.length > 1024) return finish(/* @__PURE__ */ new Error("invalid deny-token broker response"));
531
+ const end = response.indexOf("\n");
532
+ if (end < 0) return;
533
+ const line = response.slice(0, end);
534
+ if (/^OK\|[1-9][0-9]{0,19}$/.test(line)) {
535
+ const handle = BigInt(line.slice(3));
536
+ if (handle <= 18446744073709551615n) return finish(void 0, handle);
537
+ }
538
+ finish(/* @__PURE__ */ new Error(`deny-token broker refused: ${line}`));
539
+ });
540
+ });
541
+ }
542
+ /** Synchronous, bounded RPC inside the public suspended-create binding. The
543
+ * caller verifies the child's kernel token before the helper can resume it.
544
+ */
545
+ function assignDenyToken(userSid, processHandle, threadHandle, deniedIds, io) {
546
+ if (processHandle <= 0n || threadHandle <= 0n || processHandle > 18446744073709551615n || threadHandle > 18446744073709551615n || deniedIds.length < 1 || deniedIds.length > 256 || deniedIds.some((id) => !/^S-1-4-(?:[0-9]{1,10}-){4}3$/.test(id))) throw new Error("invalid deny-token assignment request");
547
+ const pipe = io.open(`\\\\.\\pipe\\${brokerPipeName(userSid)}`);
548
+ try {
549
+ const deadline = Date.now() + 1e4;
550
+ io.write(pipe, Buffer.from(`3|${processHandle}|${threadHandle}|${deniedIds.join(",")}\n`, "ascii"));
551
+ let response = "";
552
+ while (!response.includes("\n")) {
553
+ if (Date.now() > deadline) throw new Error("deny-token assignment timed out");
554
+ const count = io.available(pipe);
555
+ if (count > 1024 || response.length + count > 1024) throw new Error("invalid deny-token assignment response");
556
+ if (count > 0) response += io.read(pipe, count).toString("utf8");
557
+ else io.pause();
558
+ }
559
+ if (response.slice(0, response.indexOf("\n")) !== "OK") throw new Error(`deny-token assignment refused: ${response.trim()}`);
560
+ } finally {
561
+ io.close(pipe);
562
+ }
563
+ }
564
+ //#endregion
565
+ //#region src/windows-deny.ts
566
+ /** Ambient ACL writes minus registered read-only roots (Windows B9).
567
+ *
568
+ * The restricting list mirrors the caller's enabled identities. Inherited
569
+ * deny-only NORMAL groups identify denied roots to ordinary deny ACEs in
570
+ * the NORMAL access check, so narrowing the SID list cannot discard the deny.
571
+ * Privileges that could bypass the DACL are removed; integrity is not lowered.
572
+ * Standing ACEs retain the restriction for running children and concurrent
573
+ * snapshots. They are never revoked during runner teardown.
574
+ *
575
+ * OWNER RIGHTS suppresses owner-implicit WRITE_DAC; the actual owner receives
576
+ * that right explicitly so the unrestricted host keeps it. Parent guards stop
577
+ * FILE_DELETE_CHILD and ACL rewrites from bypassing a child's DELETE deny.
578
+ * Protected descendants get explicit ACEs (inherited denies can lose to an
579
+ * explicit allow, or fail to reach a protected DACL). Reparse points are not
580
+ * followed: targets have their own canonical policy and must not be mutated.
581
+ */
582
+ const WRITE_DAC = 262144;
583
+ const MUTATION = 852310;
584
+ async function runWindowsDenied(upstreamRunner, roots, argv, protectedHome) {
585
+ assertWindowsDenyCapacity(roots.length);
586
+ if (process.arch !== "x64" && process.arch !== "arm64") throw new Error("Windows ambient restriction requires a 64-bit process");
587
+ const verifyRoots = () => {
588
+ for (const root of roots) {
589
+ if (!isAbsolute(root) || isWindowsNetworkOrDevicePath(root)) throw new Error(`unsupported deny root: ${root}`);
590
+ if (!statSync(root).isDirectory() || canonicalPath(root).toLowerCase() !== root.toLowerCase()) throw new Error(`deny root was removed or redirected: ${root}`);
591
+ }
592
+ };
593
+ verifyRoots();
594
+ const native = await loadWindowsNative(upstreamRunner);
595
+ const api = native.extendWin32ProcessBindings(({ kernel32, advapi32, bind }) => {
596
+ return Object.fromEntries(Object.entries({
597
+ openProcess: [
598
+ kernel32,
599
+ "void*",
600
+ [
601
+ "uint32",
602
+ "int",
603
+ "uint32"
604
+ ]
605
+ ],
606
+ openProcessToken: [
607
+ advapi32,
608
+ "int",
609
+ [
610
+ "void*",
611
+ "uint32",
612
+ "void*"
613
+ ]
614
+ ],
615
+ getTokenInformation: [
616
+ advapi32,
617
+ "int",
618
+ [
619
+ "void*",
620
+ "int",
621
+ "void*",
622
+ "uint32",
623
+ "void*"
624
+ ]
625
+ ],
626
+ setTokenInformation: [
627
+ advapi32,
628
+ "int",
629
+ [
630
+ "void*",
631
+ "int",
632
+ "void*",
633
+ "uint32"
634
+ ]
635
+ ],
636
+ createRestrictedToken: [
637
+ advapi32,
638
+ "int",
639
+ [
640
+ "void*",
641
+ "uint32",
642
+ "uint32",
643
+ "void*",
644
+ "uint32",
645
+ "void*",
646
+ "uint32",
647
+ "void*",
648
+ "void*"
649
+ ]
650
+ ],
651
+ createFileW: [
652
+ kernel32,
653
+ "void*",
654
+ [
655
+ "str16",
656
+ "uint32",
657
+ "uint32",
658
+ "void*",
659
+ "uint32",
660
+ "uint32",
661
+ "void*"
662
+ ]
663
+ ],
664
+ convertStringSidToSidW: [
665
+ advapi32,
666
+ "int",
667
+ ["str16", "void*"]
668
+ ],
669
+ localFree: [
670
+ kernel32,
671
+ "void*",
672
+ ["void*"]
673
+ ],
674
+ getLengthSid: [
675
+ advapi32,
676
+ "uint32",
677
+ ["void*"]
678
+ ],
679
+ equalSid: [
680
+ advapi32,
681
+ "int",
682
+ ["void*", "void*"]
683
+ ],
684
+ convertSidToStringSidW: [
685
+ advapi32,
686
+ "int",
687
+ ["void*", "void*"]
688
+ ],
689
+ getSecurityInfo: [
690
+ advapi32,
691
+ "uint32",
692
+ [
693
+ "void*",
694
+ "int",
695
+ "uint32",
696
+ "void*",
697
+ "void*",
698
+ "void*",
699
+ "void*",
700
+ "void*"
701
+ ]
702
+ ],
703
+ setSecurityInfo: [
704
+ advapi32,
705
+ "uint32",
706
+ [
707
+ "void*",
708
+ "int",
709
+ "uint32",
710
+ "void*",
711
+ "void*",
712
+ "void*",
713
+ "void*"
714
+ ]
715
+ ],
716
+ setEntriesInAclW: [
717
+ advapi32,
718
+ "uint32",
719
+ [
720
+ "uint32",
721
+ "void*",
722
+ "void*",
723
+ "void*"
724
+ ]
725
+ ],
726
+ getFileInformationByHandle: [
727
+ kernel32,
728
+ "int",
729
+ ["void*", "void*"]
730
+ ],
731
+ convertStringSecurityDescriptorToSecurityDescriptorW: [
732
+ advapi32,
733
+ "int",
734
+ [
735
+ "str16",
736
+ "uint32",
737
+ "void*",
738
+ "void*"
739
+ ]
740
+ ],
741
+ getSecurityDescriptorDacl: [
742
+ advapi32,
743
+ "int",
744
+ [
745
+ "void*",
746
+ "void*",
747
+ "void*",
748
+ "void*"
749
+ ]
750
+ ],
751
+ initializeAcl: [
752
+ advapi32,
753
+ "int",
754
+ [
755
+ "void*",
756
+ "uint32",
757
+ "uint32"
758
+ ]
759
+ ],
760
+ getAce: [
761
+ advapi32,
762
+ "int",
763
+ [
764
+ "void*",
765
+ "uint32",
766
+ "void*"
767
+ ]
768
+ ],
769
+ addAce: [
770
+ advapi32,
771
+ "int",
772
+ [
773
+ "void*",
774
+ "uint32",
775
+ "uint32",
776
+ "void*",
777
+ "uint32"
778
+ ]
779
+ ],
780
+ getTempPathW: [
781
+ kernel32,
782
+ "uint32",
783
+ ["uint32", "void*"]
784
+ ],
785
+ getPackageFullName: [
786
+ kernel32,
787
+ "int32",
788
+ [
789
+ "void*",
790
+ "void*",
791
+ "void*"
792
+ ]
793
+ ],
794
+ writeFile: [
795
+ kernel32,
796
+ "int",
797
+ [
798
+ "void*",
799
+ "void*",
800
+ "uint32",
801
+ "void*",
802
+ "void*"
803
+ ]
804
+ ],
805
+ readFile: [
806
+ kernel32,
807
+ "int",
808
+ [
809
+ "void*",
810
+ "void*",
811
+ "uint32",
812
+ "void*",
813
+ "void*"
814
+ ]
815
+ ],
816
+ peekNamedPipe: [
817
+ kernel32,
818
+ "int",
819
+ [
820
+ "void*",
821
+ "void*",
822
+ "uint32",
823
+ "void*",
824
+ "void*",
825
+ "void*"
826
+ ]
827
+ ],
828
+ sleep: [
829
+ kernel32,
830
+ "void",
831
+ ["uint32"]
832
+ ],
833
+ lockFileEx: [
834
+ kernel32,
835
+ "int",
836
+ [
837
+ "void*",
838
+ "uint32",
839
+ "uint32",
840
+ "uint32",
841
+ "uint32",
842
+ "void*"
843
+ ]
844
+ ],
845
+ unlockFileEx: [
846
+ kernel32,
847
+ "int",
848
+ [
849
+ "void*",
850
+ "uint32",
851
+ "uint32",
852
+ "uint32",
853
+ "void*"
854
+ ]
855
+ ]
856
+ }).map(([name, [library, result, args]]) => [name, bind(library, name[0].toUpperCase() + name.slice(1), result, args)]));
857
+ });
858
+ const pointer = (buffer, offset = 0) => native.koffi.decode(buffer.subarray(offset), "void*");
859
+ const check = (ok, name) => {
860
+ if (ok === 0) throw new Error(`${name} failed (Win32 ${api.getLastError()})`);
861
+ };
862
+ const status = (code, name) => {
863
+ if (code !== 0) throw new Error(`${name} failed (Win32 ${code})`);
864
+ };
865
+ const parsedSids = [];
866
+ const sid = (text) => {
867
+ const out = Buffer.alloc(8);
868
+ check(api.convertStringSidToSidW(text, out), "ConvertStringSidToSidW");
869
+ const value = pointer(out);
870
+ parsedSids.push(value);
871
+ return value;
872
+ };
873
+ const writePointer = (buffer, offset, value) => {
874
+ buffer.writeBigUInt64LE(native.koffi.address(value), offset);
875
+ };
876
+ const info = (token, cls) => {
877
+ const size = Buffer.alloc(4);
878
+ api.getTokenInformation(token, cls, null, 0, size);
879
+ const length = size.readUInt32LE();
880
+ if (length < 8 || length > 1048576) throw new Error(`invalid TokenInformation size: ${length}`);
881
+ const data = Buffer.alloc(length);
882
+ check(api.getTokenInformation(token, cls, data, length, size), "GetTokenInformation");
883
+ return data;
884
+ };
885
+ const retained = [];
886
+ let currentToken;
887
+ let restrictedToken;
888
+ let pathGuard;
889
+ let primary;
890
+ let threw = false;
891
+ let exitCode = 0;
892
+ try {
893
+ pathGuard = pinWindowsDenyPaths(native, roots);
894
+ const preflight = preflightWindowsDenyRoots(native, roots, protectedHome);
895
+ const processHandle = api.openProcess(1024, 0, process.pid);
896
+ check(Number(processHandle !== null && processHandle !== 0), "OpenProcess");
897
+ const out = Buffer.alloc(8);
898
+ try {
899
+ check(api.openProcessToken(processHandle, 262283, out), "OpenProcessToken");
900
+ } finally {
901
+ check(api.closeHandle(processHandle), "CloseHandle(process)");
902
+ }
903
+ currentToken = pointer(out);
904
+ const user = info(currentToken, 1);
905
+ const groups = info(currentToken, 2);
906
+ retained.push(user, groups);
907
+ const userSid = pointer(user);
908
+ const identities = [userSid];
909
+ const owners = [userSid];
910
+ const count = groups.readUInt32LE();
911
+ if (count > (groups.length - 8) / 16) throw new Error("truncated TokenGroups");
912
+ for (let i = 0; i < count; i++) {
913
+ const offset = 8 + i * 16;
914
+ const attributes = groups.readUInt32LE(offset + 8);
915
+ if ((attributes & 4) === 0 || (attributes & 16) !== 0) continue;
916
+ const group = pointer(groups, offset);
917
+ identities.push(group);
918
+ if ((attributes & 8) !== 0) owners.push(group);
919
+ }
920
+ const ownerRights = sid("S-1-3-4");
921
+ const denies = roots.map(rootDenySid);
922
+ const list = Buffer.alloc(identities.length * 16);
923
+ identities.forEach((value, i) => writePointer(list, i * 16, value));
924
+ const sidOut = Buffer.alloc(8);
925
+ check(api.convertSidToStringSidW(userSid, sidOut), "ConvertSidToStringSidW");
926
+ const text = pointer(sidOut);
927
+ let minted;
928
+ let brokerUser;
929
+ try {
930
+ brokerUser = readWindowsWideString(native.koffi, text);
931
+ const handle = await requestDenyToken(brokerUser, native.koffi.address(currentToken), denies);
932
+ const bytes = Buffer.alloc(8);
933
+ bytes.writeBigUInt64LE(handle);
934
+ minted = pointer(bytes);
935
+ } finally {
936
+ api.localFree(text);
937
+ }
938
+ const restrictedOut = Buffer.alloc(8);
939
+ try {
940
+ check(api.createRestrictedToken(minted, 1, 0, null, 0, null, identities.length, list, restrictedOut), "CreateRestrictedToken");
941
+ } finally {
942
+ api.closeHandle(minted);
943
+ }
944
+ restrictedToken = pointer(restrictedOut);
945
+ const verifyDenyGroups = (token) => {
946
+ const denyGroups = info(token, 2);
947
+ const denyCount = denyGroups.readUInt32LE();
948
+ if (denyCount > (denyGroups.length - 8) / 16) throw new Error("truncated deny TokenGroups");
949
+ const verified = /* @__PURE__ */ new Set();
950
+ for (let i = 0; i < denyCount; i++) {
951
+ const offset = 8 + i * 16;
952
+ const flags = denyGroups.readUInt32LE(offset + 8);
953
+ if ((flags & 16) === 0 || (flags & 6) !== 0) continue;
954
+ const valueOut = Buffer.alloc(8);
955
+ check(api.convertSidToStringSidW(pointer(denyGroups, offset), valueOut), "ConvertSidToStringSidW(group)");
956
+ const value = pointer(valueOut);
957
+ try {
958
+ verified.add(readWindowsWideString(native.koffi, value));
959
+ } finally {
960
+ api.localFree(value);
961
+ }
962
+ }
963
+ if (denies.some((id) => !verified.has(id))) throw new Error("broker did not install the deny-only normal groups");
964
+ };
965
+ verifyDenyGroups(restrictedToken);
966
+ const entry = (trustee, mode, mask, inheritance) => {
967
+ const data = Buffer.alloc(48);
968
+ data.writeUInt32LE(mask, 0);
969
+ data.writeUInt32LE(mode, 4);
970
+ data.writeUInt32LE(inheritance, 8);
971
+ writePointer(data, 40, trustee);
972
+ return data;
973
+ };
974
+ const tokenAclOut = Buffer.alloc(8);
975
+ status(api.setEntriesInAclW(3, Buffer.concat([
976
+ entry(userSid, 3, 786656, 0),
977
+ entry(userSid, 1, 268435456, 0),
978
+ entry(ownerRights, 1, 131072, 0)
979
+ ]), null, tokenAclOut), "SetEntriesInAclW(token)");
980
+ const tokenAcl = pointer(tokenAclOut);
981
+ try {
982
+ status(api.setSecurityInfo(restrictedToken, 6, 4, null, null, tokenAcl, null), "SetSecurityInfo(token)");
983
+ const defaultDacl = Buffer.alloc(8);
984
+ writePointer(defaultDacl, 0, tokenAcl);
985
+ check(api.setTokenInformation(restrictedToken, 6, defaultDacl, 8), "SetTokenInformation(TokenDefaultDacl)");
986
+ } finally {
987
+ api.localFree(tokenAcl);
988
+ }
989
+ const tempPath = Buffer.alloc(65536);
990
+ const tempLength = api.getTempPathW(32768, tempPath);
991
+ if (tempLength === 0 || tempLength >= 32768) throw new Error("GetTempPathW failed");
992
+ const lockDirectory = join(tempPath.subarray(0, tempLength * 2).toString("utf16le"), "dsh-acl-locks");
993
+ mkdirSync(lockDirectory, { recursive: true });
994
+ const protect = (path, denyId, mask, inheritance, verifyContent = true) => {
995
+ const lockPath = join(lockDirectory, `${createHash("sha256").update(path.toLowerCase()).digest("hex").slice(0, 16)}.lock`);
996
+ const lock = api.createFileW(lockPath, 3221225472, 3, null, 4, 0, null);
997
+ if (lock === null || BigInt(lock) === -1n || BigInt(lock) === 18446744073709551615n) throw new Error(`CreateFileW(lock) failed (Win32 ${api.getLastError()})`);
998
+ const overlapped = native.koffi.alloc("uint8", 32);
999
+ native.koffi.encode(overlapped, "uint8", Buffer.alloc(32), 32);
1000
+ let held = false;
1001
+ try {
1002
+ check(api.lockFileEx(lock, 2, 0, 1, 0, overlapped), `LockFileEx(${path})`);
1003
+ held = true;
1004
+ const ownerOut = Buffer.alloc(8);
1005
+ const aclOut = Buffer.alloc(8);
1006
+ const sdOut = Buffer.alloc(8);
1007
+ const handle = api.createFileW(path, 393216, 7, null, 3, 35651584, null);
1008
+ if (handle === null || BigInt(handle) === -1n || BigInt(handle) === 18446744073709551615n) throw new Error(`CreateFileW(${path}) failed (Win32 ${api.getLastError()})`);
1009
+ if (verifyContent) try {
1010
+ preflight.verifyHandle(api, path, handle);
1011
+ } catch (error) {
1012
+ api.closeHandle(handle);
1013
+ throw error;
1014
+ }
1015
+ try {
1016
+ status(api.getSecurityInfo(handle, 1, 5, ownerOut, null, aclOut, null, sdOut), `GetSecurityInfo(${path})`);
1017
+ } catch (error) {
1018
+ api.closeHandle(handle);
1019
+ throw error;
1020
+ }
1021
+ const descriptor = pointer(sdOut);
1022
+ let merged;
1023
+ try {
1024
+ const owner = pointer(ownerOut);
1025
+ if (!owners.some((value) => api.equalSid(owner, value) !== 0)) throw new Error(`deny ACL requires caller-owned path: ${path}`);
1026
+ const oldAcl = pointer(aclOut);
1027
+ if (oldAcl === null || oldAcl === 0n) throw new Error(`deny ACL cannot preserve a null DACL: ${path}`);
1028
+ const edits = Buffer.concat([entry(ownerRights, 1, 131072, inheritance), entry(owner, 1, WRITE_DAC, inheritance)]);
1029
+ const mergedOut = Buffer.alloc(8);
1030
+ status(api.setEntriesInAclW(2, edits, oldAcl, mergedOut), `SetEntriesInAclW(${path})`);
1031
+ merged = pointer(mergedOut);
1032
+ const descriptorOut = Buffer.alloc(8);
1033
+ const flags = inheritance === 3 ? "OICI" : "";
1034
+ check(api.convertStringSecurityDescriptorToSecurityDescriptorW(`D:(D;${flags};0x${mask.toString(16)};;;${denyId})`, 1, descriptorOut, null), "ConvertStringSecurityDescriptorToSecurityDescriptorW(normal-group deny)");
1035
+ const conditionalDescriptor = pointer(descriptorOut);
1036
+ try {
1037
+ const present = Buffer.alloc(4), acl = Buffer.alloc(8), defaulted = Buffer.alloc(4);
1038
+ check(api.getSecurityDescriptorDacl(conditionalDescriptor, present, acl, defaulted), "GetSecurityDescriptorDacl");
1039
+ const first = Buffer.alloc(8);
1040
+ check(api.getAce(pointer(acl), 0, first), "GetAce(normal-group deny)");
1041
+ const header = Buffer.from(native.koffi.decode(pointer(first), "uint8", 4));
1042
+ const denyBytes = Buffer.from(native.koffi.decode(pointer(first), "uint8", header.readUInt16LE(2)));
1043
+ const count = Buffer.from(native.koffi.decode(merged, "uint8", 8)).readUInt16LE(4);
1044
+ const others = [];
1045
+ for (let index = 0; index < count; index++) {
1046
+ const ace = Buffer.alloc(8);
1047
+ check(api.getAce(merged, index, ace), "GetAce(existing)");
1048
+ const h = Buffer.from(native.koffi.decode(pointer(ace), "uint8", 4));
1049
+ const bytes = Buffer.from(native.koffi.decode(pointer(ace), "uint8", h.readUInt16LE(2)));
1050
+ if (!bytes.equals(denyBytes)) others.push(bytes);
1051
+ }
1052
+ const size = 8 + denyBytes.length + others.reduce((total, bytes) => total + bytes.length, 0);
1053
+ if (size > 65535) throw new Error(`deny ACL exceeds the Windows ACL size limit: ${path}`);
1054
+ const combined = Buffer.alloc(size);
1055
+ check(api.initializeAcl(combined, size, 4), "InitializeAcl");
1056
+ for (const bytes of [denyBytes, ...others]) check(api.addAce(combined, 4, 4294967295, bytes, bytes.length), "AddAce");
1057
+ if (verifyContent) preflight.verifyHandle(api, path, handle);
1058
+ status(api.setSecurityInfo(handle, 1, 4, null, null, combined, null), `SetSecurityInfo(${path})`);
1059
+ } finally {
1060
+ api.localFree(conditionalDescriptor);
1061
+ }
1062
+ } finally {
1063
+ if (merged !== void 0) api.localFree(merged);
1064
+ api.localFree(descriptor);
1065
+ check(api.closeHandle(handle), "CloseHandle(path)");
1066
+ }
1067
+ } finally {
1068
+ try {
1069
+ if (held) check(api.unlockFileEx(lock, 0, 1, 0, overlapped), "UnlockFileEx");
1070
+ } finally {
1071
+ native.koffi.free(overlapped);
1072
+ check(api.closeHandle(lock), "CloseHandle(lock)");
1073
+ }
1074
+ }
1075
+ };
1076
+ const expectedChildren = /* @__PURE__ */ new Map();
1077
+ for (const scanned of preflight.entries.values()) {
1078
+ const parent = dirname(scanned.path).toLowerCase();
1079
+ if (parent === scanned.path.toLowerCase()) continue;
1080
+ const names = expectedChildren.get(parent) ?? /* @__PURE__ */ new Set();
1081
+ names.add(basename(scanned.path));
1082
+ expectedChildren.set(parent, names);
1083
+ }
1084
+ const denyIdFor = new Map(roots.map((root, i) => [root.toLowerCase(), denies[i]]));
1085
+ for (const scanned of preflight.entries.values()) {
1086
+ const isPlainDirectory = (scanned.identity.attributes & 1040) === 16;
1087
+ if (isPlainDirectory) {
1088
+ let names;
1089
+ try {
1090
+ names = readdirSync(scanned.path).sort();
1091
+ } catch (error) {
1092
+ throw new Error(`deny tree changed during ACL writes: cannot list ${scanned.path} (${error instanceof Error ? error.message : String(error)})`);
1093
+ }
1094
+ const expected = expectedChildren.get(scanned.path.toLowerCase()) ?? /* @__PURE__ */ new Set();
1095
+ if (names.length !== expected.size || names.some((name) => !expected.has(name))) throw new Error(`deny tree changed since the preflight scan: ${scanned.path}`);
1096
+ }
1097
+ protect(scanned.path, denyIdFor.get(scanned.root.toLowerCase()), MUTATION, isPlainDirectory ? 3 : 0);
1098
+ }
1099
+ roots.forEach((root, i) => {
1100
+ protect(dirname(root), denies[i], 786496, 0, false);
1101
+ });
1102
+ verifyRoots();
1103
+ process.on("SIGINT", () => {});
1104
+ const control = process.env.DSH_SUBPROCESS_CONTROL === "pipe";
1105
+ const child = spawnWindowsDeniedChild(native, api, {
1106
+ token: restrictedToken,
1107
+ command: argv[0],
1108
+ args: argv.slice(1),
1109
+ cwd: process.cwd(),
1110
+ ...control ? { controlFileDescriptor: 7 } : {}
1111
+ }, {
1112
+ sourceToken: currentToken,
1113
+ assign(child, thread) {
1114
+ const packageNameLength = Buffer.alloc(4);
1115
+ const packageProbe = api.getPackageFullName(child, packageNameLength, null);
1116
+ if (packageProbe === 122) {
1117
+ const nameBuffer = Buffer.alloc(packageNameLength.readUInt32LE() * 2);
1118
+ const read = api.getPackageFullName(child, packageNameLength, nameBuffer);
1119
+ const fullName = read === 0 ? nameBuffer.subarray(0, (packageNameLength.readUInt32LE() - 1) * 2).toString("utf16le") : `<unreadable, Win32 ${read}>`;
1120
+ throw new Error(`deny launch refused: ${argv[0]} started as packaged (MSIX/Microsoft Store) application ${fullName}; a packaged process carries a package identity the deny token cannot preserve. Install the MSI or ZIP build of PowerShell (or use a non-packaged executable) and put it ahead of the WindowsApps alias on PATH`);
1121
+ }
1122
+ if (packageProbe !== 15700) throw new Error(`GetPackageFullName(child) failed (Win32 ${packageProbe})`);
1123
+ assignDenyToken(brokerUser, native.koffi.address(child), native.koffi.address(thread), denies, {
1124
+ open(pipe) {
1125
+ const handle = api.createFileW(pipe, 3221225472, 0, null, 3, 0, null);
1126
+ if (handle === null || BigInt(handle) === -1n || BigInt(handle) === 18446744073709551615n) throw new Error(`Open broker assignment pipe failed (Win32 ${api.getLastError()})`);
1127
+ return handle;
1128
+ },
1129
+ write(pipe, bytes) {
1130
+ const count = Buffer.alloc(4);
1131
+ check(api.writeFile(pipe, bytes, bytes.length, count, null), "WriteFile(broker)");
1132
+ if (count.readUInt32LE() !== bytes.length) throw new Error("incomplete broker assignment write");
1133
+ },
1134
+ available(pipe) {
1135
+ const count = Buffer.alloc(4);
1136
+ check(api.peekNamedPipe(pipe, null, 0, null, count, null), "PeekNamedPipe(broker)");
1137
+ return count.readUInt32LE();
1138
+ },
1139
+ read(pipe, length) {
1140
+ const data = Buffer.alloc(length), count = Buffer.alloc(4);
1141
+ check(api.readFile(pipe, data, length, count, null), "ReadFile(broker)");
1142
+ if (count.readUInt32LE() === 0) throw new Error("broker assignment disconnected");
1143
+ return data.subarray(0, count.readUInt32LE());
1144
+ },
1145
+ pause() {
1146
+ api.sleep(10);
1147
+ },
1148
+ close(pipe) {
1149
+ api.closeHandle(pipe);
1150
+ }
1151
+ });
1152
+ const out = Buffer.alloc(8);
1153
+ check(api.openProcessToken(child, 8, out), "OpenProcessToken(assigned child)");
1154
+ const token = pointer(out);
1155
+ try {
1156
+ verifyDenyGroups(token);
1157
+ } finally {
1158
+ api.closeHandle(token);
1159
+ }
1160
+ }
1161
+ });
1162
+ let waitError;
1163
+ let waitThrew = false;
1164
+ let waited = false;
1165
+ try {
1166
+ if (control) closeSync(7);
1167
+ waited = true;
1168
+ exitCode = native.waitForProcessExit(api, child.process);
1169
+ } catch (error) {
1170
+ waitError = error;
1171
+ waitThrew = true;
1172
+ }
1173
+ const drainErrors = [];
1174
+ try {
1175
+ terminateWindowsJobAndWait(native, api, child.job);
1176
+ } catch (error) {
1177
+ drainErrors.push(error instanceof Error ? error : new Error(String(error)));
1178
+ }
1179
+ try {
1180
+ check(api.closeHandle(child.job), "CloseHandle(job)");
1181
+ } catch (error) {
1182
+ drainErrors.push(error instanceof Error ? error : new Error(String(error)));
1183
+ }
1184
+ if (!waited) try {
1185
+ check(api.closeHandle(child.process), "CloseHandle(child process)");
1186
+ } catch (error) {
1187
+ drainErrors.push(error instanceof Error ? error : new Error(String(error)));
1188
+ }
1189
+ if (waitThrew || drainErrors.length > 0) {
1190
+ const all = [...waitThrew ? [waitError instanceof Error ? waitError : new Error(String(waitError))] : [], ...drainErrors];
1191
+ if (all.length === 1) throw all[0];
1192
+ throw new AggregateError(all, all[0].message);
1193
+ }
1194
+ } catch (error) {
1195
+ primary = error;
1196
+ threw = true;
1197
+ }
1198
+ const cleanupErrors = [];
1199
+ const attempt = (operation) => {
1200
+ try {
1201
+ operation();
1202
+ } catch (error) {
1203
+ cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
1204
+ }
1205
+ };
1206
+ attempt(() => {
1207
+ if (restrictedToken !== void 0) check(api.closeHandle(restrictedToken), "CloseHandle(restricted token)");
1208
+ });
1209
+ attempt(() => {
1210
+ if (currentToken !== void 0) check(api.closeHandle(currentToken), "CloseHandle(token)");
1211
+ });
1212
+ for (const value of parsedSids) attempt(() => api.localFree(value));
1213
+ retained.length = 0;
1214
+ attempt(() => pathGuard?.close());
1215
+ if (threw) {
1216
+ if (cleanupErrors.length === 0) throw primary;
1217
+ throw new AggregateError([primary instanceof Error ? primary : new Error(String(primary)), ...cleanupErrors], primary instanceof Error ? primary.message : String(primary));
1218
+ }
1219
+ if (cleanupErrors.length === 1) throw cleanupErrors[0];
1220
+ if (cleanupErrors.length > 1) throw new AggregateError(cleanupErrors, "Windows ambient deny cleanup failed");
1221
+ return exitCode;
1222
+ }
1223
+ //#endregion
5
1224
  //#region src/windows-runner.ts
6
- /** Standalone runner: public upstream AclSandbox with a capability for the whole root set. */
1225
+ /**
1226
+ * Standalone runner: public upstream AclSandbox with a capability for the
1227
+ * whole root set.
1228
+ *
1229
+ * This file is built into `lib/windows-runner.js` and executed as a SEPARATE
1230
+ * Node process. The host's in-process plugin package resolution does not
1231
+ * apply there, so this module (and everything it statically imports — only
1232
+ * `windows-runner-contract.ts`) may import `node:` builtins exclusively. The
1233
+ * upstream ACL package is reached dynamically through `--upstream-runner`:
1234
+ * `createRequire(<upstream runner>).resolve('@deepseek-ai/dsh-sandbox-windows-acl')
1235
+ * resolves the package by self-reference from its own install location — the
1236
+ * one anchor guaranteed to see it.
1237
+ */
7
1238
  async function main() {
8
1239
  if (process.platform !== "win32") throw new Error("Windows ACL runner requires Windows");
9
1240
  const raw = process.argv.slice(2);
10
1241
  const separator = raw.indexOf("--");
11
1242
  if (separator < 0 || raw[separator + 1] === void 0) throw new Error("missing command after --");
12
1243
  const profile = parseWindowsProfile(raw.slice(0, separator), true);
13
- const roots = [.../* @__PURE__ */ new Set([profile.workspace, ...profile.additionalRoots])];
14
- for (const root of roots) if (!statSync(root).isDirectory() || canonicalPath(root) !== root) throw new Error(`workspace root was removed or redirected: ${root}`);
15
- const { AclSandbox, assertTempRootOutsideWorkspace, workspaceWriteSid, tempWriteSid } = await import("@deepseek-ai/dsh-sandbox-windows-acl");
1244
+ if (profile.mode === "danger-full-access") return await runWindowsDenied(profile.upstreamRunner, profile.deniedRoots, raw.slice(separator + 1), profile.protectedHome);
1245
+ const roots = profile.mode === "read-only" ? [...new Set(profile.additionalRoots)] : [.../* @__PURE__ */ new Set([profile.workspace, ...profile.additionalRoots])];
1246
+ const verifyRoots = () => {
1247
+ for (const root of roots) {
1248
+ if (isWindowsNetworkOrDevicePath(root)) throw new Error(`Windows ACL confinement cannot cover network or device paths: ${root}`);
1249
+ if (!statSync(root).isDirectory() || canonicalPath(root) !== root) throw new Error(`workspace root was removed or redirected: ${root}`);
1250
+ }
1251
+ };
1252
+ verifyRoots();
1253
+ if (!existsSync(profile.temp) || !statSync(profile.temp).isDirectory()) throw new Error(`--temp is not an existing directory: ${profile.temp}`);
1254
+ const aclEntry = createRequire(profile.upstreamRunner).resolve("@deepseek-ai/dsh-sandbox-windows-acl");
1255
+ const { AclSandbox, assertTempRootOutsideWorkspace, workspaceWriteSid, tempWriteSid } = await import(pathToFileURL(aclEntry).href);
16
1256
  if (profile.writeSid !== void 0 && (profile.writeSid !== workspaceWriteSid(profile.workspace) || profile.tempWriteSid !== tempWriteSid(profile.temp))) throw new Error("upstream capability SIDs do not match their paths");
17
1257
  for (const root of roots) assertTempRootOutsideWorkspace(root, profile.temp);
18
- const temp = mkdtempSync(join(profile.temp, "dsh-multi-root-"));
1258
+ const temp = profile.mode === "workspace-write" ? mkdtempSync(join(profile.temp, "dsh-multi-root-")) : null;
19
1259
  const sandbox = new AclSandbox({
20
1260
  writableDirs: roots,
21
1261
  tempDir: temp,
22
1262
  writeSid: workspaceScopeSid(roots),
23
- tempWriteSid: tempWriteSid(temp),
1263
+ ...temp === null ? {} : { tempWriteSid: tempWriteSid(temp) },
24
1264
  mode: "workspace-write"
25
1265
  });
26
1266
  let initialized = false;
27
1267
  try {
28
1268
  await sandbox.init();
29
1269
  initialized = true;
30
- process.env.TMP = temp;
31
- process.env.TEMP = temp;
1270
+ verifyRoots();
1271
+ if (temp !== null) {
1272
+ process.env.TMP = temp;
1273
+ process.env.TEMP = temp;
1274
+ }
32
1275
  process.on("SIGINT", () => {});
33
1276
  const control = process.env.DSH_SUBPROCESS_CONTROL === "pipe";
34
1277
  const child = sandbox.spawn({
@@ -46,7 +1289,7 @@ async function main() {
46
1289
  reportCleanup(error);
47
1290
  }
48
1291
  try {
49
- rmSync(temp, {
1292
+ if (temp !== null) rmSync(temp, {
50
1293
  recursive: true,
51
1294
  force: true
52
1295
  });