@deepseek-ai/dsh-win32-process 0.1.5-rc.2 → 0.1.6-alpha.2

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.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/subprocess/win32-process/README.md
5
- README.md: 7a4d12f7fe6ef553113e0f202886fd42e8a313a2
6
- README.zh.md: 8ebc614d38ea206cba792b90a2f118c028bf0447
5
+ README.md: 155ca3807791a83fef437af67312b3480ce7d2a2
6
+ README.zh.md: f0c4439af27ef45c88d73bcba54851d789da063d
package/README.md CHANGED
@@ -32,8 +32,12 @@ This low-level Win32 process library is consumed by the Windows ACL sandbox and
32
32
  - **Ordinary settlement operations** — `pollProcessExit()` publishes direct exit separately, while `isJobEmpty()` reads `QueryInformationJobObject(JobObjectBasicAccountingInformation)` until `ActiveProcesses` reaches zero. Checked Job termination and handle closure keep the runner as the only native owner.
33
33
  - **Explicit settlement ownership** — `waitForProcessExit()` waits and closes a sandbox process handle; ordinary runner process polling, Job accounting, and checked Job termination/closure remain separate operations. `drainPipe()` reuses one native count slot while draining, frees it, and closes the pipe read handle. Each caller owns its result composition and returned handles.
34
34
 
35
+ Process creation sets `STARTF_USESHOWWINDOW` with `SW_HIDE` before target code runs. It preserves console inheritance and does not add `CREATE_NO_WINDOW` or `CREATE_NEW_CONSOLE`, which can fail DLL initialization under the restricted token. Existing parent console windows are not hidden.
36
+
35
37
  The Windows ACL sandbox adds SID, DACL, grant, workspace, and public child policy above these primitives.
36
38
 
39
+ - **Inherited control descriptor** — Job creation accepts an optional fd-7 pipe. `STARTUPINFO.cbReserved2/lpReserved2` carries an eight-slot CRT descriptor table with standard handles, closed slots 3–6, and the control pipe at slot 7. The table is allocated until CreateProcess returns; temporary handle inheritance is reset on success and failure. Initializing the slot before Node starts avoids overwriting descriptors Node has already allocated.
40
+
37
41
  <a id="header-verification"></a>
38
42
  ## Header verification
39
43
 
package/README.zh.md CHANGED
@@ -9,33 +9,37 @@ kind: "package-library"
9
9
 
10
10
  ## 概述
11
11
 
12
- 供 Windows ACL 沙箱与普通子进程 Job runner 消费的底层 Win32 进程库。它唯一拥有仓库中可复用 process、stdio 与 Job Object 操作的 Koffi 绑定表;它不是 Cordis 服务,也不决定沙箱策略或公共 child 行为。维护任一原生进程路径或检查 handle 生命周期限制时,请阅读本页。
12
+ 供 Windows ACL 沙箱与普通子进程 Job runner 消费的底层 Win32 进程库。它唯一拥有仓库中可复用 process、stdio 与 Job Object 操作的 Koffi 绑定表;它不是 Cordis 服务,也不决定沙箱策略或公共 child 行为。维护任一原生进程路径或检查句柄生命周期限制时,请阅读本页。
13
13
 
14
14
  ## 目录
15
15
 
16
- - [Behavior](#behavior)
17
- - [头部验证](#header-verification)
18
- - [Model Experience](#model-experience)
19
- - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
16
+ - [行为](#behavior)
17
+ - [头文件验证](#header-verification)
18
+ - [模型体验](#model-experience)
19
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
20
20
  - [开发备注](#dev-note)
21
21
 
22
22
  -----
23
23
 
24
24
  <a id="behavior"></a>
25
- ## Behavior
25
+ ## 行为
26
26
 
27
- - **唯一可复用 ABI owner** — `abi.ts` 拥有两条 process 路径消费的 Win32 常量与 x64 布局值。`ffi.ts` 懒加载 `kernel32.dll` 与 `advapi32.dll`,核验 `STARTUPINFOW` 和 `PROCESS_INFORMATION`,提供带类型的操作与错误格式化,并让 sandbox policy 通过同一组已加载库绑定剩余 API。
28
- - **restricted-token 创建** — `RestrictedProcessSpawnOptions` 要求 sandbox 的 primary token,并使用 `CreateProcessAsUserW`。pipe 与 inherited-stdio 路径共用命令行引用、cwd、restricted-token 空环境策略、返回值检查与句柄清理。
27
+ - **唯一可复用 ABI owner** — `abi.ts` 拥有两条 process 路径消费的 Win32 常量与 x64 布局值。`ffi.ts` 懒加载 `kernel32.dll` 与 `advapi32.dll`,核验 `STARTUPINFOW` 和 `PROCESS_INFORMATION`,提供带类型的操作与错误格式化,并让沙箱策略通过同一组已加载库绑定剩余 API。
28
+ - **restricted-token 创建** — `RestrictedProcessSpawnOptions` 要求沙箱的 primary token,并使用 `CreateProcessAsUserW`。pipe 与 inherited-stdio 路径共用命令行引号处理、cwd、restricted-token null 环境策略、返回值检查与句柄清理。
29
29
  - **管道进程原语** — `spawnPipedProcess()` 创建匿名 stdin/stdout/stderr 管道,立即关闭 stdin,并返回两个读取端;调用方负责等待进程与排空管道。任一局部失败都会关闭该操作已经拥有的句柄,并在各自 Win32 生命周期结束后释放每个 Koffi 输出槽与结构体分配。
30
30
  - **继承 stdio 的 Job 原语** — `spawnInheritedJobProcess()` 创建一个 kill-on-close Job,临时把当前 stdio 句柄设为可继承,以 suspended 状态创建 restricted child,把它分配给 Job,再恢复初始线程。目标代码不会在 Job 分配前运行;受控的分配或恢复失败会终止 suspended child,或在释放全部已拥有句柄前关闭已分配的 Job。
31
- - **ordinary Job runner 原语** — `CurrentTokenProcessSpawnOptions` 要求已解析的 `applicationName`、完整 target 环境,以及三个专用于 target stdin、stdout 与 stderr 的 runner CRT 描述符。`spawnCurrentTokenJobProcess()` 通过 Node 导出的 `uv_get_osfhandle()` 把这些描述符映射为 OS handle,拒绝无效结果,临时把 handle 设为可继承,并通过 `STARTF_USESTDHANDLES` 传入。它使用 `CREATE_UNICODE_ENVIRONMENT` 传入排序后的 UTF-16LE 环境块,再以 suspended 状态通过 `CreateProcessW` 创建 target、把它分配给 unnamed kill-on-close Job,并只在分配后恢复。原始命令行 argv 项保持不变,runner 也可以关闭自己的 carrier 描述符,而不触碰 Node 自身的标准流。
32
- - **ordinary 停稳操作** — `pollProcessExit()` 单独发布 direct exit,`isJobEmpty()` 则读取 `QueryInformationJobObject(JobObjectBasicAccountingInformation)`,直到 `ActiveProcesses` 归零。带检查的 Job 终止与 handle 关闭使 runner 保持唯一 native owner。
33
- - **显式结算归属** — `waitForProcessExit()` 等待并关闭 sandbox process handle;ordinary runner 的 process polling、Job accounting 与 checked Job termination/closure 是独立操作。`drainPipe()` 在排空期间复用一个 native count slot,释放该分配并关闭管道读取句柄。每个调用方拥有自己的 result 组合与返回 handle。
31
+ - **ordinary Job runner 原语** — `CurrentTokenProcessSpawnOptions` 要求已解析的 `applicationName`、完整 target 环境,以及三个专用于 target stdin、stdout 与 stderr 的 runner CRT 描述符。`spawnCurrentTokenJobProcess()` 通过 Node 导出的 `uv_get_osfhandle()` 把这些描述符映射为 OS 句柄,拒绝无效结果,临时把句柄设为可继承,并通过 `STARTF_USESTDHANDLES` 传入。它使用 `CREATE_UNICODE_ENVIRONMENT` 传入排序后的 UTF-16LE 环境块,再以 suspended 状态通过 `CreateProcessW` 创建 target、把它分配给 unnamed kill-on-close Job,并只在分配后恢复。原始命令行 argv 项保持不变,runner 也可以关闭自己的 carrier 描述符,而不触碰 Node 自身的标准流。
32
+ - **ordinary 结算操作** — `pollProcessExit()` 单独发布 direct exit,`isJobEmpty()` 则读取 `QueryInformationJobObject(JobObjectBasicAccountingInformation)`,直到 `ActiveProcesses` 归零。带检查的 Job 终止与句柄关闭使 runner 保持唯一 native owner。
33
+ - **显式结算归属** — `waitForProcessExit()` 等待并关闭沙箱 process 句柄;ordinary runner 的 process polling、Job accounting 与 checked Job termination/closure 是独立操作。`drainPipe()` 在排空期间复用一个 native count slot,释放该分配并关闭管道读取句柄。每个调用方拥有自己的 result 组合与返回句柄。
34
34
 
35
- Windows ACL 沙箱在这些原语上增加 SID、DACL、grant、workspace 与公共 child policy。
35
+ 进程创建在目标代码运行前设置 `STARTF_USESHOWWINDOW` 和 `SW_HIDE`。它保留控制台继承,不添加可能导致受限令牌下 DLL 初始化失败的 `CREATE_NO_WINDOW` 或 `CREATE_NEW_CONSOLE`。已有的父进程控制台窗口不会被隐藏。
36
+
37
+ Windows ACL 沙箱在这些原语上增加 SID、DACL、grant、workspace 与公共 child 策略。
38
+
39
+ - **继承控制描述符**——Job 创建接受可选的 fd-7 管道。`STARTUPINFO.cbReserved2/lpReserved2` 携带八槽 CRT 描述符表,其中包含标准句柄、关闭的槽 3–6,以及槽 7 的控制管道。该表保留到 CreateProcess 返回;临时句柄继承在成功和失败时均恢复。在 Node 启动前初始化该槽可避免覆盖 Node 已分配的描述符。
36
40
 
37
41
  <a id="header-verification"></a>
38
- ## 头部验证
42
+ ## 头文件验证
39
43
 
40
44
  process、stdio 与 Job 的常量以及选定结构体的大小和偏移由 [`verify/abi-probe.cpp`](verify/abi-probe.cpp) 对照 MinGW Windows 头文件检查:
41
45
 
@@ -43,16 +47,16 @@ process、stdio 与 Job 的常量以及选定结构体的大小和偏移由 [`ve
43
47
  g++ -std=c++20 -municode -O2 -o abi-probe.exe verify/abi-probe.cpp && ./abi-probe.exe
44
48
  ```
45
49
 
46
- Koffi 的 `STARTUPINFOW` 与 `PROCESS_INFORMATION` 定义还会在模块加载时断言各自的 64 位大小。该探针还固定指针与 handle 宽度、Unicode 环境标志,以及用于判断停稳的基础 Job accounting record 大小与 `ActiveProcesses` 偏移;其余已记录偏移和常量也由该探针提供证据。
50
+ Koffi 的 `STARTUPINFOW` 与 `PROCESS_INFORMATION` 定义还会在模块加载时断言各自的 64 位大小。该探针还固定指针与句柄宽度、Unicode 环境标志,以及用于判断完全停稳的基础 Job accounting record 大小与 `ActiveProcesses` 偏移;其余已记录偏移和常量也由该探针提供证据。
47
51
 
48
52
  <a id="model-experience"></a>
49
- ## Model Experience
53
+ ## 模型体验
50
54
 
51
55
  ### 进程原语
52
56
 
53
57
  #### 模型看到什么
54
58
 
55
- 没有直接内容。本包向 sandbox 与 ordinary runner 提供 `Win32ProcessBindings`、`CurrentTokenProcessBindings` 与进程原语;两者拥有全部模型可见工具、输出与诊断,本包不贡献提示词或工具 schema。
59
+ 没有直接内容。本包向沙箱与 ordinary runner 提供 `Win32ProcessBindings`、`CurrentTokenProcessBindings` 与进程原语;两者拥有全部模型可见工具、输出与诊断,本包不贡献提示词或工具 schema。
56
60
 
57
61
  #### Token 影响
58
62
 
@@ -62,14 +66,14 @@ Koffi 的 `STARTUPINFOW` 与 `PROCESS_INFORMATION` 定义还会在模块加载
62
66
 
63
67
  本包不贡献稳定请求前缀,因此不会使模型 KV Cache 失效。
64
68
 
65
- ## Known Limitations and Deferred Work
69
+ ## 已知限制与延期工作
66
70
 
67
71
  <a id="known-limitations-and-deferred-work"></a>
68
72
 
69
73
  - **仅在 Windows 原生加载** — 导入通用类型可跨平台进行,但解析绑定表会加载 Windows DLL,并在其他宿主失败。跨平台测试注入绑定表,不加载原生 API。
70
- - **没有公共进程服务** — 本包刻意不把原语包装成 Cordis 或 Node streams。消费方必须拥有自己的策略、异步调度、输出上限、取消与最终句柄关闭。
71
- - **restricted-token 空环境** — `CreateProcessAsUserW` sandbox 原语传入空环境块,并先通过 `SetEnvironmentVariableW` 建立改动,因为经 Koffi 传入显式环境块会以 `ERROR_INVALID_PARAMETER` 失败。ordinary `CreateProcessW` runner 则要求完整 target 环境,并传入排序、双 NUL 结尾的 UTF-16LE 块,其中包括 `=X:` 驱动器条目,而不修改自身环境。
72
- - **没有 standalone process API** — 本包只暴露当前 sandbox 与 ordinary-runner consumer 所需的操作,不拥有 Node streams、公共 handle、output policy、cancellation 或 durable state。
74
+ - **没有公共进程服务** — 本包刻意不把原语包装成 Cordis 或 Node 流。消费方必须拥有自己的策略、异步调度、输出上限、取消与最终句柄关闭。
75
+ - **restricted-token null 环境** — `CreateProcessAsUserW` 沙箱原语传入 null 环境块,并先通过 `SetEnvironmentVariableW` 建立改动,因为经 Koffi 传入显式环境块会以 `ERROR_INVALID_PARAMETER` 失败。ordinary `CreateProcessW` runner 则要求完整 target 环境,并传入排序、双 NUL 结尾的 UTF-16LE 块,其中包括 `=X:` 驱动器条目,而不修改自身环境。
76
+ - **没有 standalone process API** — 本包只暴露当前沙箱与 ordinary-runner 消费方所需的操作,不拥有 Node 流、公共句柄、输出策略、取消或 durable state。
73
77
  - **创建到分配之间的中断** — 目标以 suspended 状态启动,不能在 Job 分配前执行,但 runner 若在进程创建到分配之间的极窄区间被外力终止,可能留下 suspended target。本包不声明原子 Job 附加保证。
74
78
  - **header 证据限定架构** — 已提交的 ABI probe 与布局常量覆盖仓库当前 64 位 Windows 目标。支持新的指针宽度或不兼容 Windows ABI 前,必须先更新 probe。
75
79
 
@@ -84,4 +88,4 @@ Koffi 的 `STARTUPINFOW` 与 `PROCESS_INFORMATION` 定义还会在模块加载
84
88
 
85
89
  </details>
86
90
 
87
- **运行时不变式:** 不发布伴生入口。操作只持有调用内的 native handle。
91
+ **运行时不变式:** 不发布伴生入口。操作只持有调用内的原生句柄。
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import koffi from "koffi";
1
+ import { createLazyRequire } from "@deepseek-ai/dsh-lazy-require";
2
2
  /** Win32 code reporting a caller-provided buffer is too small. */
3
3
  const ERROR_INSUFFICIENT_BUFFER = 122;
4
4
  /** Job limit that terminates every member when the final Job handle closes. */
@@ -19,10 +19,13 @@ var Win32Error = class extends Error {
19
19
  }
20
20
  };
21
21
  //#endregion
22
+ //#region lib/types/koffi.js
23
+ /** Process-realm lazy access to Koffi's CommonJS entry. */
24
+ /** Load Koffi on the first Win32 native operation. */
25
+ const requireKoffi = createLazyRequire("koffi", import.meta.url);
26
+ //#endregion
22
27
  //#region lib/types/ffi.js
23
28
  /** Lazy Koffi bindings for generic Win32 process, stdio, and Job operations. */
24
- const PVOID = koffi.pointer("void");
25
- const PPVOID = koffi.pointer(PVOID);
26
29
  /**
27
30
  * Return whether a Koffi pointer represents NULL.
28
31
  * @param value - pointer value returned by Koffi or a Win32 call.
@@ -31,51 +34,64 @@ const PPVOID = koffi.pointer(PVOID);
31
34
  function isNullPtr(value) {
32
35
  return value === null || value === void 0 || value === 0n;
33
36
  }
34
- /** Koffi STARTUPINFOW layout. */
35
- const STARTUPINFOW = koffi.struct("DSH_STARTUPINFOW", {
36
- cb: "uint32",
37
- lpReserved: "str16",
38
- lpDesktop: "str16",
39
- lpTitle: "str16",
40
- dwX: "uint32",
41
- dwY: "uint32",
42
- dwXSize: "uint32",
43
- dwYSize: "uint32",
44
- dwXCountChars: "uint32",
45
- dwYCountChars: "uint32",
46
- dwFillAttribute: "uint32",
47
- dwFlags: "uint32",
48
- wShowWindow: "uint16",
49
- cbReserved2: "uint16",
50
- lpReserved2: koffi.pointer("uint8"),
51
- hStdInput: PVOID,
52
- hStdOutput: PVOID,
53
- hStdError: PVOID
54
- });
55
- /** Koffi PROCESS_INFORMATION layout. */
56
- const PROCESS_INFORMATION = koffi.struct("DSH_PROCESS_INFORMATION", {
57
- hProcess: PVOID,
58
- hThread: PVOID,
59
- dwProcessId: "uint32",
60
- dwThreadId: "uint32"
61
- });
62
- /* v8 ignore start -- ABI guards are pinned by native header probes. */
63
- if (STARTUPINFOW.size !== 104) throw new Error(`STARTUPINFOW layout mismatch: koffi computed ${STARTUPINFOW.size}, expected 104`);
64
- if (PROCESS_INFORMATION.size !== 24) throw new Error(`PROCESS_INFORMATION layout mismatch: koffi computed ${PROCESS_INFORMATION.size}, expected 24`);
65
- /* v8 ignore stop */
37
+ let cachedTypes;
38
+ /** Resolve Koffi pointer and process layouts on the first native operation. */
39
+ function win32Types() {
40
+ if (cachedTypes !== void 0) return cachedTypes;
41
+ const koffi = requireKoffi();
42
+ const PVOID = koffi.pointer("void");
43
+ const PPVOID = koffi.pointer(PVOID);
44
+ const STARTUPINFOW = koffi.struct("DSH_STARTUPINFOW", {
45
+ cb: "uint32",
46
+ lpReserved: "str16",
47
+ lpDesktop: "str16",
48
+ lpTitle: "str16",
49
+ dwX: "uint32",
50
+ dwY: "uint32",
51
+ dwXSize: "uint32",
52
+ dwYSize: "uint32",
53
+ dwXCountChars: "uint32",
54
+ dwYCountChars: "uint32",
55
+ dwFillAttribute: "uint32",
56
+ dwFlags: "uint32",
57
+ wShowWindow: "uint16",
58
+ cbReserved2: "uint16",
59
+ lpReserved2: koffi.pointer("uint8"),
60
+ hStdInput: PVOID,
61
+ hStdOutput: PVOID,
62
+ hStdError: PVOID
63
+ });
64
+ const PROCESS_INFORMATION = koffi.struct("DSH_PROCESS_INFORMATION", {
65
+ hProcess: PVOID,
66
+ hThread: PVOID,
67
+ dwProcessId: "uint32",
68
+ dwThreadId: "uint32"
69
+ });
70
+ /* v8 ignore start -- ABI guards are pinned by native header probes. */
71
+ if (STARTUPINFOW.size !== 104) throw new Error(`STARTUPINFOW layout mismatch: koffi computed ${STARTUPINFOW.size}, expected 104`);
72
+ if (PROCESS_INFORMATION.size !== 24) throw new Error(`PROCESS_INFORMATION layout mismatch: koffi computed ${PROCESS_INFORMATION.size}, expected 24`);
73
+ /* v8 ignore stop */
74
+ return cachedTypes = {
75
+ PVOID,
76
+ PPVOID,
77
+ STARTUPINFOW,
78
+ PROCESS_INFORMATION
79
+ };
80
+ }
66
81
  /**
67
82
  * Allocate a pointer-sized out-parameter slot.
68
83
  * @returns allocated native slot.
69
84
  */
70
85
  function allocPtrSlot() {
71
- return koffi.alloc(PVOID, 1);
86
+ const { PVOID } = win32Types();
87
+ return requireKoffi().alloc(PVOID, 1);
72
88
  }
73
89
  /**
74
90
  * Allocate a uint32 out-parameter slot.
75
91
  * @returns allocated native slot.
76
92
  */
77
93
  function allocUint32() {
78
- return koffi.alloc("uint32", 1);
94
+ return requireKoffi().alloc("uint32", 1);
79
95
  }
80
96
  /**
81
97
  * Decode a pointer out-parameter.
@@ -83,7 +99,7 @@ function allocUint32() {
83
99
  * @returns decoded pointer, or null for address zero.
84
100
  */
85
101
  function decodePtr(slot) {
86
- const value = koffi.decode(slot, PVOID);
102
+ const value = requireKoffi().decode(slot, win32Types().PVOID);
87
103
  return isNullPtr(value) ? null : value;
88
104
  }
89
105
  /**
@@ -92,14 +108,14 @@ function decodePtr(slot) {
92
108
  * @returns decoded unsigned value.
93
109
  */
94
110
  function decodeUint32(slot) {
95
- return koffi.decode(slot, "uint32");
111
+ return requireKoffi().decode(slot, "uint32");
96
112
  }
97
113
  /**
98
114
  * Allocate a zeroed STARTUPINFOW.
99
115
  * @returns allocated struct pointer.
100
116
  */
101
117
  function allocStartupInfo() {
102
- return koffi.alloc(STARTUPINFOW, 1);
118
+ return requireKoffi().alloc(win32Types().STARTUPINFOW, 1);
103
119
  }
104
120
  /**
105
121
  * Encode the stdio-bearing STARTUPINFOW fields.
@@ -107,14 +123,14 @@ function allocStartupInfo() {
107
123
  * @param fields - fields required for inherited stdio.
108
124
  */
109
125
  function encodeStartupInfo(startupInfo, fields) {
110
- koffi.encode(startupInfo, STARTUPINFOW, fields);
126
+ requireKoffi().encode(startupInfo, win32Types().STARTUPINFOW, fields);
111
127
  }
112
128
  /**
113
129
  * Allocate a zeroed PROCESS_INFORMATION.
114
130
  * @returns allocated struct pointer.
115
131
  */
116
132
  function allocProcessInfo() {
117
- return koffi.alloc(PROCESS_INFORMATION, 1);
133
+ return requireKoffi().alloc(win32Types().PROCESS_INFORMATION, 1);
118
134
  }
119
135
  /**
120
136
  * Decode PROCESS_INFORMATION.
@@ -122,13 +138,14 @@ function allocProcessInfo() {
122
138
  * @returns process/thread handles and ids.
123
139
  */
124
140
  function decodeProcessInfo(processInfo) {
125
- return koffi.decode(processInfo, PROCESS_INFORMATION);
141
+ return requireKoffi().decode(processInfo, win32Types().PROCESS_INFORMATION);
126
142
  }
127
143
  let cachedContext;
128
144
  let cached;
129
145
  /* v8 ignore start -- exercised by native Windows ABI and sandbox jobs. */
130
146
  function bindingContext() {
131
147
  if (cachedContext !== void 0) return cachedContext;
148
+ const koffi = requireKoffi();
132
149
  const kernel32 = koffi.load("kernel32.dll");
133
150
  const advapi32 = koffi.load("advapi32.dll");
134
151
  const bind = (lib, name, result, args) => lib.func("__stdcall", name, result, args);
@@ -141,11 +158,14 @@ function bindingContext() {
141
158
  }
142
159
  function bindings() {
143
160
  if (cached !== void 0) return cached;
161
+ const koffi = requireKoffi();
162
+ const { PVOID, PPVOID, STARTUPINFOW, PROCESS_INFORMATION } = win32Types();
144
163
  const { kernel32, advapi32, bind } = bindingContext();
145
164
  const node = koffi.load(null);
146
165
  cached = {
147
166
  closeHandle: bind(kernel32, "CloseHandle", "int", [PVOID]),
148
167
  getLastError: bind(kernel32, "GetLastError", "uint32", []),
168
+ getFileType: bind(kernel32, "GetFileType", "uint32", [PVOID]),
149
169
  formatMessageW: bind(kernel32, "FormatMessageW", "uint32", [
150
170
  "uint32",
151
171
  PVOID,
@@ -284,6 +304,44 @@ function throwWin32(api, name, win32Code, detail) {
284
304
  throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code));
285
305
  }
286
306
  //#endregion
307
+ //#region lib/types/control-stdio.js
308
+ /** Windows CRT startup descriptors for a Node payload with one inherited control pipe. */
309
+ const HANDLE_BYTES = 8;
310
+ const INVALID_HANDLE = 18446744073709551615n;
311
+ const FOPEN = 1;
312
+ const FPIPE = 8;
313
+ const FDEV = 64;
314
+ const FILE_TYPE_CHAR = 2;
315
+ const FILE_TYPE_PIPE = 3;
316
+ /**
317
+ * Encode the CRT's descriptor table before the child runtime allocates descriptors.
318
+ * Supported Windows targets use 64-bit handles. Empty slots remain closed, and the
319
+ * backing Buffer must remain alive until CreateProcess returns.
320
+ * @param api - native file-type inspection for inherited handles.
321
+ * @param stdio - standard handles and the provider-owned fd-7 control pipe.
322
+ * @returns descriptor count, flag bytes, and handle values for STARTUPINFO's reserved CRT fields.
323
+ */
324
+ function inheritedControlStdio(api, stdio) {
325
+ const count = stdio.control.fileDescriptor + 1;
326
+ const handleOffset = 4 + count;
327
+ const bytes = Buffer.alloc(handleOffset + count * HANDLE_BYTES);
328
+ bytes.writeUInt32LE(count, 0);
329
+ for (let index = 0; index < count; index++) bytes.writeBigUInt64LE(INVALID_HANDLE, handleOffset + index * HANDLE_BYTES);
330
+ const entries = [
331
+ [0, stdio.stdin],
332
+ [1, stdio.stdout],
333
+ [2, stdio.stderr],
334
+ [stdio.control.fileDescriptor, stdio.control.handle]
335
+ ];
336
+ for (const [fd, handle] of entries) {
337
+ const kind = api.getFileType(handle);
338
+ if (fd === stdio.control.fileDescriptor && kind !== FILE_TYPE_PIPE) throw new Error("subprocess control descriptor is not a Windows pipe");
339
+ bytes[4 + fd] = FOPEN | (kind === FILE_TYPE_PIPE ? FPIPE : kind === FILE_TYPE_CHAR ? FDEV : 0);
340
+ bytes.writeBigUInt64LE(handle, handleOffset + fd * HANDLE_BYTES);
341
+ }
342
+ return bytes;
343
+ }
344
+ //#endregion
287
345
  //#region lib/types/process.js
288
346
  /** Typed Win32 process operations over the shared binding table. */
289
347
  /**
@@ -326,7 +384,7 @@ function encodeWindowsEnvironment(env) {
326
384
  return Buffer.from(`${strings.join("\0")}\0\0`, "utf16le");
327
385
  }
328
386
  function freeNative(pointer) {
329
- if (pointer !== void 0) koffi.free(pointer);
387
+ if (pointer !== void 0) requireKoffi().free(pointer);
330
388
  }
331
389
  function closeBestEffort(api, handle) {
332
390
  if (!isNullPtr(handle)) api.closeHandle(handle);
@@ -352,7 +410,7 @@ function createPipe(api, owned) {
352
410
  };
353
411
  } finally {
354
412
  freeNative(writeSlot);
355
- koffi.free(readSlot);
413
+ requireKoffi().free(readSlot);
356
414
  }
357
415
  }
358
416
  function closeOwned(api, owned, handle) {
@@ -369,6 +427,7 @@ function createRestrictedProcess(api, options, commandLine, creationFlags, start
369
427
  }
370
428
  /**
371
429
  * Spawn a process with anonymous-pipe stdout/stderr and immediate stdin EOF.
430
+ * New console windows start hidden without changing console inheritance.
372
431
  * @param api - active binding table.
373
432
  * @param options - command, cwd, args, and restricted primary token.
374
433
  * @returns caller-owned process and pipe read handles.
@@ -389,7 +448,8 @@ function spawnPipedProcess(api, options) {
389
448
  startupInfo = allocStartupInfo();
390
449
  encodeStartupInfo(startupInfo, {
391
450
  cb: 104,
392
- dwFlags: 256,
451
+ dwFlags: 257,
452
+ wShowWindow: 0,
393
453
  hStdInput: stdIn.read,
394
454
  hStdOutput: stdOut.write,
395
455
  hStdError: stdErr.write
@@ -488,7 +548,7 @@ function createKillOnCloseJob(api) {
488
548
  }
489
549
  const UV_INVALID_OS_FILE_HANDLE = 18446744073709551615n;
490
550
  const UV_INVALID_FILE_DESCRIPTOR = 18446744073709551614n;
491
- function inheritedStandardHandles(api) {
551
+ function inheritedStandardHandles(api, controlFileDescriptor) {
492
552
  const get = (selector, label) => {
493
553
  const handle = api.getStdHandle(selector);
494
554
  if (!isNullPtr(handle)) return handle;
@@ -497,19 +557,27 @@ function inheritedStandardHandles(api) {
497
557
  return {
498
558
  stdin: get(-10, "stdin"),
499
559
  stdout: get(-11, "stdout"),
500
- stderr: get(-12, "stderr")
560
+ stderr: get(-12, "stderr"),
561
+ ...controlFileDescriptor === void 0 ? {} : { control: {
562
+ fileDescriptor: controlFileDescriptor,
563
+ handle: descriptorHandle(api, controlFileDescriptor, "control")
564
+ } }
501
565
  };
502
566
  }
567
+ function descriptorHandle(api, fileDescriptor, label) {
568
+ const handle = api.uvGetOsfhandle(fileDescriptor);
569
+ if (isNullPtr(handle) || handle === UV_INVALID_OS_FILE_HANDLE || handle === UV_INVALID_FILE_DESCRIPTOR) throw new Error(`uv_get_osfhandle returned an invalid handle for target ${label} fd ${String(fileDescriptor)}`);
570
+ return handle;
571
+ }
503
572
  function targetCarrierHandles(api, descriptors) {
504
- const get = (fileDescriptor, label) => {
505
- const handle = api.uvGetOsfhandle(fileDescriptor);
506
- if (isNullPtr(handle) || handle === UV_INVALID_OS_FILE_HANDLE || handle === UV_INVALID_FILE_DESCRIPTOR) throw new Error(`uv_get_osfhandle returned an invalid handle for target ${label} fd ${String(fileDescriptor)}`);
507
- return handle;
508
- };
509
573
  return {
510
- stdin: get(descriptors.stdin, "stdin"),
511
- stdout: get(descriptors.stdout, "stdout"),
512
- stderr: get(descriptors.stderr, "stderr")
574
+ stdin: descriptorHandle(api, descriptors.stdin, "stdin"),
575
+ stdout: descriptorHandle(api, descriptors.stdout, "stdout"),
576
+ stderr: descriptorHandle(api, descriptors.stderr, "stderr"),
577
+ ...descriptors.control === void 0 ? {} : { control: {
578
+ fileDescriptor: descriptors.control,
579
+ handle: descriptorHandle(api, descriptors.control, "control")
580
+ } }
513
581
  };
514
582
  }
515
583
  /** Shared suspended-create, Job-assignment, and resume lifecycle. */
@@ -518,25 +586,45 @@ function spawnJobProcess(api, options, resolveStdio, createName, create) {
518
586
  const enabled = [];
519
587
  let startupInfo;
520
588
  let processInfo;
589
+ let controlDescriptorBlock;
521
590
  let created = 0;
522
591
  let createFailureCode = 0;
523
592
  try {
524
593
  const stdio = resolveStdio();
525
- for (const [handle, label] of [
594
+ const inherited = [
526
595
  [stdio.stdin, "stdin"],
527
596
  [stdio.stdout, "stdout"],
528
597
  [stdio.stderr, "stderr"]
529
- ]) {
598
+ ];
599
+ if (stdio.control !== void 0) inherited.push([stdio.control.handle, "control"]);
600
+ for (const [handle, label] of inherited) {
530
601
  if (api.setHandleInformation(handle, 1, 1) === 0) throwLastError(api, "SetHandleInformation", `${label} (enable inherit)`);
531
602
  enabled.push(handle);
532
603
  }
604
+ const controlBytes = stdio.control === void 0 ? void 0 : inheritedControlStdio(api, {
605
+ ...stdio,
606
+ control: stdio.control
607
+ });
608
+ if (controlBytes !== void 0) {
609
+ const koffi = requireKoffi();
610
+ controlDescriptorBlock = {
611
+ pointer: koffi.alloc("uint8", controlBytes.length),
612
+ length: controlBytes.length
613
+ };
614
+ koffi.encode(controlDescriptorBlock.pointer, "uint8", controlBytes, controlBytes.length);
615
+ }
533
616
  startupInfo = allocStartupInfo();
534
617
  encodeStartupInfo(startupInfo, {
535
618
  cb: 104,
536
- dwFlags: 256,
619
+ dwFlags: 257,
620
+ wShowWindow: 0,
537
621
  hStdInput: stdio.stdin,
538
622
  hStdOutput: stdio.stdout,
539
- hStdError: stdio.stderr
623
+ hStdError: stdio.stderr,
624
+ ...controlDescriptorBlock === void 0 ? {} : {
625
+ cbReserved2: controlDescriptorBlock.length,
626
+ lpReserved2: controlDescriptorBlock.pointer
627
+ }
540
628
  });
541
629
  processInfo = allocProcessInfo();
542
630
  created = create(startupInfo, processInfo);
@@ -547,6 +635,7 @@ function spawnJobProcess(api, options, resolveStdio, createName, create) {
547
635
  throw error;
548
636
  } finally {
549
637
  freeNative(startupInfo);
638
+ freeNative(controlDescriptorBlock?.pointer);
550
639
  for (const handle of enabled) api.setHandleInformation(handle, 1, 0);
551
640
  }
552
641
  if (created === 0) {
@@ -590,7 +679,7 @@ function spawnJobProcess(api, options, resolveStdio, createName, create) {
590
679
  };
591
680
  }
592
681
  /**
593
- * Spawn a restricted-token process suspended, assign its Job, then resume it.
682
+ * Spawn a restricted-token process suspended with hidden initial windows, assign its Job, then resume it.
594
683
  * @param api - active binding table.
595
684
  * @param options - command, cwd, args, and restricted primary token.
596
685
  * @returns caller-owned process and Job handles after successful resume.
@@ -601,10 +690,10 @@ function spawnJobProcess(api, options, resolveStdio, createName, create) {
601
690
  */
602
691
  function spawnInheritedJobProcess(api, options) {
603
692
  const commandLine = buildCommandLine(options.command, options.args);
604
- return spawnJobProcess(api, options, () => inheritedStandardHandles(api), "CreateProcessAsUserW", (startupInfo, processInfo) => createRestrictedProcess(api, options, commandLine, 4, startupInfo, processInfo));
693
+ return spawnJobProcess(api, options, () => inheritedStandardHandles(api, options.controlFileDescriptor), "CreateProcessAsUserW", (startupInfo, processInfo) => createRestrictedProcess(api, options, commandLine, 4, startupInfo, processInfo));
605
694
  }
606
695
  /**
607
- * Spawn an ordinary process suspended, assign its Job, then resume it.
696
+ * Spawn an ordinary process suspended with hidden initial windows, assign its Job, then resume it.
608
697
  * @param api - active binding table.
609
698
  * @param options - command, cwd, argv, and target carrier descriptors.
610
699
  * @returns caller-owned process and Job handles after successful resume.
@@ -636,7 +725,7 @@ function pollProcessExit(api, process) {
636
725
  if (api.getExitCodeProcess(process, exitCodeSlot) === 0) throwLastError(api, "GetExitCodeProcess");
637
726
  return decodeUint32(exitCodeSlot);
638
727
  } finally {
639
- koffi.free(exitCodeSlot);
728
+ requireKoffi().free(exitCodeSlot);
640
729
  }
641
730
  }
642
731
  /**
@@ -1,6 +1,10 @@
1
1
  /** Generic Win32 process, stdio, and Job Object constants verified on x64. */
2
2
  /** STARTUPINFOW uses the standard input, output, and error handles. */
3
3
  export declare const STARTF_USESTDHANDLES = 256;
4
+ /** STARTUPINFOW applies wShowWindow when creating a console window. */
5
+ export declare const STARTF_USESHOWWINDOW = 1;
6
+ /** Initial window visibility that preserves the child's console attachment. */
7
+ export declare const SW_HIDE = 0;
4
8
  /** HandleInformation flag that permits child inheritance. */
5
9
  export declare const HANDLE_FLAG_INHERIT = 1;
6
10
  /** Infinite WaitForSingleObject timeout. */
@@ -0,0 +1,22 @@
1
+ /** Windows CRT startup descriptors for a Node payload with one inherited control pipe. */
2
+ import type { NativePtr, Win32ProcessBindings } from './ffi.ts';
3
+ /** Native stdio handles plus the single additional descriptor selected by the launcher. */
4
+ export interface InheritedControlStdio {
5
+ stdin: NativePtr;
6
+ stdout: NativePtr;
7
+ stderr: NativePtr;
8
+ control: {
9
+ fileDescriptor: 7;
10
+ handle: NativePtr;
11
+ };
12
+ }
13
+ /**
14
+ * Encode the CRT's descriptor table before the child runtime allocates descriptors.
15
+ * Supported Windows targets use 64-bit handles. Empty slots remain closed, and the
16
+ * backing Buffer must remain alive until CreateProcess returns.
17
+ * @param api - native file-type inspection for inherited handles.
18
+ * @param stdio - standard handles and the provider-owned fd-7 control pipe.
19
+ * @returns descriptor count, flag bytes, and handle values for STARTUPINFO's reserved CRT fields.
20
+ */
21
+ export declare function inheritedControlStdio(api: Pick<Win32ProcessBindings, 'getFileType'>, stdio: InheritedControlStdio): Buffer;
22
+ //# sourceMappingURL=control-stdio.d.ts.map
@@ -1,19 +1,19 @@
1
1
  /** Lazy Koffi bindings for generic Win32 process, stdio, and Job operations. */
2
- import koffi from 'koffi';
2
+ import { type Koffi } from './koffi.ts';
3
3
  declare const nativePtr: unique symbol;
4
4
  /** Koffi native pointer branded against accidental numeric use. */
5
5
  export type NativePtr = bigint & {
6
6
  readonly [nativePtr]: true;
7
7
  };
8
- type Ptr = ReturnType<typeof koffi.pointer>;
8
+ type Ptr = ReturnType<Koffi['pointer']>;
9
9
  /** Loaded Win32 libraries and the shared stdcall binder used by process extensions. */
10
10
  export interface Win32BindingContext {
11
11
  /** Kernel process, handle, pipe, and Job APIs. */
12
- readonly kernel32: ReturnType<typeof koffi.load>;
12
+ readonly kernel32: ReturnType<Koffi['load']>;
13
13
  /** Token and security APIs. */
14
- readonly advapi32: ReturnType<typeof koffi.load>;
14
+ readonly advapi32: ReturnType<Koffi['load']>;
15
15
  /** Bind one stdcall function from a loaded Win32 library. */
16
- readonly bind: (library: ReturnType<typeof koffi.load>, name: string, result: Ptr | string, args: Array<Ptr | string>) => unknown;
16
+ readonly bind: (library: ReturnType<Koffi['load']>, name: string, result: Ptr | string, args: Array<Ptr | string>) => unknown;
17
17
  }
18
18
  /**
19
19
  * Return whether a Koffi pointer represents NULL.
@@ -25,9 +25,12 @@ export declare function isNullPtr(value: NativePtr | null | undefined): value is
25
25
  export interface StartupInfoInput {
26
26
  cb: number;
27
27
  dwFlags: number;
28
+ wShowWindow: number;
28
29
  hStdInput: NativePtr;
29
30
  hStdOutput: NativePtr;
30
31
  hStdError: NativePtr;
32
+ cbReserved2?: number;
33
+ lpReserved2?: NativePtr;
31
34
  }
32
35
  /** Decoded PROCESS_INFORMATION result. */
33
36
  export interface ProcessInfoOutput {
@@ -40,6 +43,8 @@ export interface ProcessInfoOutput {
40
43
  export interface Win32ProcessBindings {
41
44
  closeHandle(handle: NativePtr): number;
42
45
  getLastError(): number;
46
+ getFileType(handle: NativePtr): number;
47
+ uvGetOsfhandle(fileDescriptor: number): NativePtr | null;
43
48
  formatMessageW(flags: number, source: null, messageId: number, languageId: number, buffer: Buffer, size: number, args: null): number;
44
49
  createPipe(readHandle: NativePtr, writeHandle: NativePtr, attributes: null, size: number): number;
45
50
  setHandleInformation(handle: NativePtr, mask: number, flags: number): number;
@@ -62,10 +67,16 @@ export interface Win32ProcessBindings {
62
67
  export interface CurrentTokenProcessBindings extends Win32ProcessBindings {
63
68
  uvGetOsfhandle(fileDescriptor: number): NativePtr | null;
64
69
  }
65
- /** Koffi STARTUPINFOW layout. */
66
- export declare const STARTUPINFOW: import("koffi").TypeObject;
67
- /** Koffi PROCESS_INFORMATION layout. */
68
- export declare const PROCESS_INFORMATION: import("koffi").TypeObject;
70
+ /**
71
+ * Materialize the Koffi STARTUPINFOW layout on first native use.
72
+ * @returns the cached native struct type.
73
+ */
74
+ export declare function startupInfoType(): ReturnType<Koffi['struct']>;
75
+ /**
76
+ * Materialize the Koffi PROCESS_INFORMATION layout on first native use.
77
+ * @returns the cached native struct type.
78
+ */
79
+ export declare function processInformationType(): ReturnType<Koffi['struct']>;
69
80
  /**
70
81
  * Allocate a pointer-sized out-parameter slot.
71
82
  * @returns allocated native slot.
@@ -0,0 +1,49 @@
1
+ /** Process-realm lazy access to Koffi's CommonJS entry. */
2
+ import type koffi from 'koffi';
3
+ /** Koffi runtime export type. */
4
+ export type Koffi = typeof koffi;
5
+ /** Load Koffi on the first Win32 native operation. */
6
+ export declare const requireKoffi: () => {
7
+ LibraryHandle: import("koffi").LibraryHandle;
8
+ TypeObject: import("koffi").TypeObject;
9
+ Union: import("koffi").Union;
10
+ address: typeof import("koffi").address;
11
+ alias: typeof import("koffi").alias;
12
+ alignof: typeof import("koffi").alignof;
13
+ alloc: typeof import("koffi").alloc;
14
+ array: typeof import("koffi").array;
15
+ as: typeof import("koffi").as;
16
+ call: typeof import("koffi").call;
17
+ config: typeof import("koffi").config;
18
+ decode: typeof import("koffi").decode;
19
+ disposable: typeof import("koffi").disposable;
20
+ encode: typeof import("koffi").encode;
21
+ enumeration: typeof import("koffi").enumeration;
22
+ errno: typeof import("koffi").errno;
23
+ extension: typeof import("koffi").extension;
24
+ free: typeof import("koffi").free;
25
+ inout: typeof import("koffi").inout;
26
+ introspect: typeof import("koffi").introspect;
27
+ load: typeof import("koffi").load;
28
+ node: typeof import("koffi").node;
29
+ offsetof: typeof import("koffi").offsetof;
30
+ opaque: typeof import("koffi").opaque;
31
+ os: typeof import("koffi").os;
32
+ out: typeof import("koffi").out;
33
+ pack: typeof import("koffi").pack;
34
+ pointer: typeof import("koffi").pointer;
35
+ proto: typeof import("koffi").proto;
36
+ register: typeof import("koffi").register;
37
+ reset: typeof import("koffi").reset;
38
+ resolve: typeof import("koffi").resolve;
39
+ sizeof: typeof import("koffi").sizeof;
40
+ stats: typeof import("koffi").stats;
41
+ struct: typeof import("koffi").struct;
42
+ type: typeof import("koffi").type;
43
+ types: typeof import("koffi").types;
44
+ union: typeof import("koffi").union;
45
+ unregister: typeof import("koffi").unregister;
46
+ version: typeof import("koffi").version;
47
+ view: typeof import("koffi").view;
48
+ };
49
+ //# sourceMappingURL=koffi.d.ts.map
@@ -35,11 +35,15 @@ export interface CurrentTokenStdioFileDescriptors {
35
35
  stdin: number;
36
36
  stdout: number;
37
37
  stderr: number;
38
+ /** Optional carrier and target descriptor for the inherited control pipe. */
39
+ control?: 7;
38
40
  }
39
41
  /** Restricted-token process creation inputs owned by the Windows ACL sandbox. */
40
42
  export interface RestrictedProcessSpawnOptions extends ProcessSpawnOptions {
41
43
  /** Restricted primary token supplied by sandbox policy. */
42
44
  token: NativePtr;
45
+ /** Optional control pipe inherited at the same descriptor in the payload. */
46
+ controlFileDescriptor?: 7;
43
47
  }
44
48
  /** Piped child resources whose process and read handles remain caller-owned. */
45
49
  export interface SpawnedPipedProcess {
@@ -63,6 +67,7 @@ export interface SpawnedJobProcess {
63
67
  }
64
68
  /**
65
69
  * Spawn a process with anonymous-pipe stdout/stderr and immediate stdin EOF.
70
+ * New console windows start hidden without changing console inheritance.
66
71
  * @param api - active binding table.
67
72
  * @param options - command, cwd, args, and restricted primary token.
68
73
  * @returns caller-owned process and pipe read handles.
@@ -84,7 +89,7 @@ export declare function drainPipe(api: Win32ProcessBindings, handle: NativePtr):
84
89
  */
85
90
  export declare function waitForProcessExit(api: Win32ProcessBindings, process: NativePtr): number;
86
91
  /**
87
- * Spawn a restricted-token process suspended, assign its Job, then resume it.
92
+ * Spawn a restricted-token process suspended with hidden initial windows, assign its Job, then resume it.
88
93
  * @param api - active binding table.
89
94
  * @param options - command, cwd, args, and restricted primary token.
90
95
  * @returns caller-owned process and Job handles after successful resume.
@@ -95,7 +100,7 @@ export declare function waitForProcessExit(api: Win32ProcessBindings, process: N
95
100
  */
96
101
  export declare function spawnInheritedJobProcess(api: Win32ProcessBindings, options: RestrictedProcessSpawnOptions): SpawnedJobProcess;
97
102
  /**
98
- * Spawn an ordinary process suspended, assign its Job, then resume it.
103
+ * Spawn an ordinary process suspended with hidden initial windows, assign its Job, then resume it.
99
104
  * @param api - active binding table.
100
105
  * @param options - command, cwd, argv, and target carrier descriptors.
101
106
  * @returns caller-owned process and Job handles after successful resume.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-win32-process",
3
3
  "description": "Shared low-level Win32 process, stdio, and Job Object primitives",
4
- "version": "0.1.5-rc.2",
4
+ "version": "0.1.6-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,7 +30,8 @@
30
30
  "@deepseek-ai/cordis": "^4.0.2"
31
31
  },
32
32
  "dependencies": {
33
- "koffi": "^3.1.0"
33
+ "koffi": "^3.1.0",
34
+ "@deepseek-ai/dsh-lazy-require": "^0.1.6-alpha.2"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@deepseek-ai/cordis": "^4.0.2"