@deepseek-ai/dsh-sandbox-windows-acl 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 +2 -2
- package/README.md +2 -0
- package/README.zh.md +5 -3
- package/lib/index.js +1 -1
- package/lib/runner.js +9 -5
- package/lib/types/index.d.ts +2 -0
- package/lib/types/spawn.d.ts +1 -0
- package/lib/{types-DuU3lSVe.js → types-CutH1Lgc.js} +24 -11
- package/package.json +10 -6
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/sandbox/sandbox-windows-acl/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 30eaea27b9f8a0ebd1691348c9e6f87706891866
|
|
6
|
+
README.zh.md: eeb65e5db1044eb75e0c13c5f3c76667432dc485
|
package/README.md
CHANGED
|
@@ -107,6 +107,8 @@ node runner.js --workspace <dir> --temp <dir> --mode <read-only|workspace-write>
|
|
|
107
107
|
|
|
108
108
|
The seam materializes the deterministic workspace SID's ACE standing (once per workspace per server lifetime — the reuse cache), then creates a random private temp directory and a distinct revocable SID for each live session/workspace pair, passing both as the required `--write-sid`/`--temp-write-sid` pair; the runner verifies each against its owning path and neither grants nor revokes (`manageDacls: false`). A fork receives a different temp capability, and a fresh provider gives even the same resumed session a new path and SID, so crash residue is inert litter. Without the pair, `--temp` names a root: an agentless workspace-write runner creates a random private child, self-manages its temp SID, rewrites TMP/TEMP, and removes the child on exit. Re-granting the standing workspace ACE after a restart is idempotent: `grantWrite` reads the current DACL and skips the re-propagation when the exact ACE already stands. A workspace equal to or containing the temp root is rejected before any grant.
|
|
109
109
|
|
|
110
|
+
When launched with the subprocess control marker, the runner forwards fd 7 through the restricted child's CRT startup table and closes its own copy immediately after spawn. The optional `controlFileDescriptor: 7` input requires `stdio: 'inherit'`; requesting it with piped stdio fails before process creation.
|
|
111
|
+
|
|
110
112
|
### Verified boundaries
|
|
111
113
|
|
|
112
114
|
- **Everyone grants remain ambient write authority** — Everyone must stay in both restricting lists (removing it breaks early DLL initialization and CNG); an external NTFS object whose DACL grants Everyone a requested write right clears both checks and stays writable under both modes.
|
package/README.zh.md
CHANGED
|
@@ -63,7 +63,7 @@ sandbox.dispose() // revokes the revocable (temp) grant, keeps the standing work
|
|
|
63
63
|
rmSync(tempDir, { recursive: true, force: true })
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
工作区 ACE 以常驻方式授予——`dispose()` 保留它们,因为它们是跨实例的复用缓存——而不同的临时 SID
|
|
66
|
+
工作区 ACE 以常驻方式授予——`dispose()` 保留它们,因为它们是跨实例的复用缓存——而不同的临时 SID 以可回收方式授予。服务端对应实现是 `AclWriteGrant` 类:每个目录一次 `add(path, standing)`,`dispose()` 撤销可回收路径并释放 SID。
|
|
67
67
|
|
|
68
68
|
### 隔离给你带来什么
|
|
69
69
|
|
|
@@ -107,6 +107,8 @@ node runner.js --workspace <dir> --temp <dir> --mode <read-only|workspace-write>
|
|
|
107
107
|
|
|
108
108
|
seam 先把确定性工作区 SID 的 ACE 常驻物化(每个工作区每服务器生命周期一次——复用缓存),再为每个活跃的会话/工作区对创建随机私有临时目录和不同的可回收 SID,把两种身份作为必须成对出现的 `--write-sid`/`--temp-write-sid` 传入;runner 对照各自所属路径验证二者,既不授权也不撤销(`manageDacls: false`)。fork 获得不同的临时能力;即使恢复的是同一会话,新的提供方也会给出新的路径和 SID,因此崩溃残留只是失效垃圾。如果不带这一对标志,`--temp` 指定的是根目录:无 agent(智能体)/独立的 workspace-write runner 会创建随机私有子目录,自行管理其临时 SID,重写 TMP/TEMP,并在退出时移除该子目录。重启后重新授权常驻工作区 ACE 是幂等的:`grantWrite` 读取当前 DACL,当完全相同的 ACE 已存在时跳过重新传播。工作区若等于或包含临时根目录,会在任何授权前被拒绝。
|
|
109
109
|
|
|
110
|
+
启动时若带有 subprocess 控制标记,runner 会通过受限子进程的 CRT 启动表转发 fd 7,并在 spawn 后立即关闭自身副本。可选的 `controlFileDescriptor: 7` 输入要求 `stdio: 'inherit'`;在管道 stdio 下请求它会在进程创建前失败。
|
|
111
|
+
|
|
110
112
|
### 已验证边界
|
|
111
113
|
|
|
112
114
|
- **Everyone 授权仍是环境中的写权限来源。** Everyone 必须保留在两种 restricting 列表中(移除它会破坏早期 DLL 初始化与 CNG);外部 NTFS 对象若其 DACL 向 Everyone 授予所请求的写权限,就会同时通过两次检查,并在两种模式下保持可写。
|
|
@@ -119,13 +121,13 @@ seam 先把确定性工作区 SID 的 ACE 常驻物化(每个工作区每服
|
|
|
119
121
|
- **受限子进程的临时能力按每个活跃的会话/工作区对私有。** runner 在 spawn 之前把 TMP/TEMP 改写为该私有目录;共享同一工作区 SID 的两个令牌无法写入彼此的临时目录。
|
|
120
122
|
- **受限令牌下 `whoami` 与令牌检查 cmdlet 会失败。** 子进程对复制令牌的 `GetTokenInformation` 部分不可用,这是诊断噪音而非运行故障。
|
|
121
123
|
|
|
122
|
-
###
|
|
124
|
+
### 头文件验证与源码索引
|
|
123
125
|
|
|
124
126
|
沙箱拥有的 SID、ACL、令牌、文件与锁声明由 [`verify/abi-probe.cpp`](verify/abi-probe.cpp) 对照 Windows 头文件检查。共享进程、stdio 与 Job ABI 由 [`@deepseek-ai/dsh-win32-process`](../../subprocess/win32-process/README.zh.md#header-verification) 归属并验证。
|
|
125
127
|
|
|
126
128
|
| 文件 | 职责 |
|
|
127
129
|
|---|---|
|
|
128
|
-
| [`src/index.ts`](src/index.ts) | `AclSandbox`:受限令牌策略、DACL
|
|
130
|
+
| [`src/index.ts`](src/index.ts) | `AclSandbox`:受限令牌策略、DACL 授权、fail-closed 的 spawn 与 dispose |
|
|
129
131
|
| [`src/runner.ts`](src/runner.ts) | 基于共享 Win32 进程原语的 runner 入口 |
|
|
130
132
|
| [`src/grant.ts`](src/grant.ts) | `AclWriteGrant`:服务端授权物化与撤销 |
|
|
131
133
|
| [`src/token.ts`](src/token.ts) + [`src/acl.ts`](src/acl.ts) | 沙箱背后的 Win32 令牌与 DACL 原语 |
|
package/lib/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as assertTempRootOutsideWorkspace, i as AclWriteGrant, n as tempWriteSid, r as workspaceWriteSid, t as AclSandbox } from "./types-
|
|
1
|
+
import { a as assertTempRootOutsideWorkspace, i as AclWriteGrant, n as tempWriteSid, r as workspaceWriteSid, t as AclSandbox } from "./types-CutH1Lgc.js";
|
|
2
2
|
export { AclSandbox, AclWriteGrant, assertTempRootOutsideWorkspace, tempWriteSid, workspaceWriteSid };
|
package/lib/runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as assertTempRootOutsideWorkspace, n as tempWriteSid, o as win32, r as workspaceWriteSid, t as AclSandbox } from "./types-
|
|
2
|
-
import { existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
|
1
|
+
import { a as assertTempRootOutsideWorkspace, n as tempWriteSid, o as win32, r as workspaceWriteSid, t as AclSandbox } from "./types-CutH1Lgc.js";
|
|
2
|
+
import { closeSync, existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { SUBPROCESS_CONTROL_ENV, SUBPROCESS_CONTROL_FD } from "@deepseek-ai/dsh-subprocess/control";
|
|
4
5
|
//#region lib/types/runner.js
|
|
5
6
|
/**
|
|
6
7
|
* The windows-acl confinement runner: the argv-prefix wrapper the sandbox
|
|
@@ -153,11 +154,14 @@ async function main() {
|
|
|
153
154
|
if (api.setEnvironmentVariableW("TMP", privateTempDir) === 0) fail(`SetEnvironmentVariableW TMP failed (Win32 ${api.getLastError()})`);
|
|
154
155
|
if (api.setEnvironmentVariableW("TEMP", privateTempDir) === 0) fail(`SetEnvironmentVariableW TEMP failed (Win32 ${api.getLastError()})`);
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
const child = sandbox.spawn({
|
|
157
158
|
command: parsed.command,
|
|
158
159
|
args: parsed.args,
|
|
159
|
-
stdio: "inherit"
|
|
160
|
-
|
|
160
|
+
stdio: "inherit",
|
|
161
|
+
...process.env[SUBPROCESS_CONTROL_ENV] === "pipe" ? { controlFileDescriptor: SUBPROCESS_CONTROL_FD } : {}
|
|
162
|
+
});
|
|
163
|
+
if (process.env[SUBPROCESS_CONTROL_ENV] === "pipe") closeSync(SUBPROCESS_CONTROL_FD);
|
|
164
|
+
return (await child.wait()).exitCode;
|
|
161
165
|
} finally {
|
|
162
166
|
if (initialized) try {
|
|
163
167
|
sandbox?.dispose();
|
package/lib/types/index.d.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface AclSandboxSpawnOptions {
|
|
|
98
98
|
* child dies with the caller; stdout/stderr in the result are empty.
|
|
99
99
|
*/
|
|
100
100
|
stdio?: 'pipe' | 'inherit';
|
|
101
|
+
/** Control pipe forwarded to the same payload descriptor in inherited-stdio mode. */
|
|
102
|
+
controlFileDescriptor?: 7;
|
|
101
103
|
}
|
|
102
104
|
/** A settled confined child: captured stdio and the exit code. */
|
|
103
105
|
export interface AclSandboxChildResult {
|
package/lib/types/spawn.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare function spawnSandboxedInherited(api: Win32Bindings, token: Nativ
|
|
|
31
31
|
command: string;
|
|
32
32
|
args: readonly string[];
|
|
33
33
|
cwd: string;
|
|
34
|
+
controlFileDescriptor?: 7;
|
|
34
35
|
}): SpawnedInherited;
|
|
35
36
|
/**
|
|
36
37
|
* Wait for a restricted child and close its process handle.
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { ERROR_INSUFFICIENT_BUFFER, Win32Error, allocPtrSlot, allocUint32, decodePtr, decodeUint32, drainPipe, extendWin32ProcessBindings, isNullPtr, isNullPtr as isNullPtr$1, spawnInheritedJobProcess, spawnPipedProcess, throwLastError, throwLastError as throwLastError$1, throwWin32, waitForProcessExit } from "@deepseek-ai/dsh-win32-process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import
|
|
5
|
+
import { createLazyRequire } from "@deepseek-ai/dsh-lazy-require";
|
|
6
6
|
//#region lib/types/win32-abi.js
|
|
7
7
|
/** ACL/token-specific Win32 constants. */
|
|
8
8
|
/** OpenProcess access required to query the current process token. */
|
|
@@ -20,8 +20,17 @@ const FILE_ALL_ACCESS = 2032127;
|
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region lib/types/ffi.js
|
|
22
22
|
/** ACL/token bindings layered on the shared Win32 process owner. */
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
const requireKoffi = createLazyRequire("koffi", import.meta.url);
|
|
24
|
+
let cachedTypes;
|
|
25
|
+
function ffiTypes() {
|
|
26
|
+
if (cachedTypes !== void 0) return cachedTypes;
|
|
27
|
+
const koffi = requireKoffi();
|
|
28
|
+
const PVOID = koffi.pointer("void");
|
|
29
|
+
return cachedTypes = {
|
|
30
|
+
PVOID,
|
|
31
|
+
PPVOID: koffi.pointer(PVOID)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
25
34
|
/**
|
|
26
35
|
* Return whether CreateFileW produced INVALID_HANDLE_VALUE.
|
|
27
36
|
* @param handle - handle returned by CreateFileW.
|
|
@@ -37,7 +46,7 @@ function isInvalidHandle(handle) {
|
|
|
37
46
|
* @param value - unsigned value to store.
|
|
38
47
|
*/
|
|
39
48
|
function encodeUint32(slot, value) {
|
|
40
|
-
|
|
49
|
+
requireKoffi().encode(slot, "uint32", value);
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
43
52
|
* Return a Koffi pointer's numeric address for struct packing.
|
|
@@ -45,7 +54,7 @@ function encodeUint32(slot, value) {
|
|
|
45
54
|
* @returns pointer address.
|
|
46
55
|
*/
|
|
47
56
|
function ptrAddress(ptr) {
|
|
48
|
-
return
|
|
57
|
+
return requireKoffi().address(ptr);
|
|
49
58
|
}
|
|
50
59
|
/**
|
|
51
60
|
* Allocate a raw byte block.
|
|
@@ -53,7 +62,7 @@ function ptrAddress(ptr) {
|
|
|
53
62
|
* @returns allocated pointer.
|
|
54
63
|
*/
|
|
55
64
|
function allocBytes(length) {
|
|
56
|
-
return
|
|
65
|
+
return requireKoffi().alloc("uint8", length);
|
|
57
66
|
}
|
|
58
67
|
/**
|
|
59
68
|
* Allocate one zeroed x64 OVERLAPPED record.
|
|
@@ -71,7 +80,7 @@ function allocOverlapped() {
|
|
|
71
80
|
* @returns decoded pointer, or null for address zero.
|
|
72
81
|
*/
|
|
73
82
|
function decodePtrAt(buffer, offset) {
|
|
74
|
-
const value =
|
|
83
|
+
const value = requireKoffi().decode(buffer, offset, ffiTypes().PVOID);
|
|
75
84
|
return isNullPtr(value) ? null : value;
|
|
76
85
|
}
|
|
77
86
|
/**
|
|
@@ -81,7 +90,7 @@ function decodePtrAt(buffer, offset) {
|
|
|
81
90
|
* @returns decoded value.
|
|
82
91
|
*/
|
|
83
92
|
function decodeUint8At(ptr, offset) {
|
|
84
|
-
return
|
|
93
|
+
return requireKoffi().decode(ptr, offset, "uint8");
|
|
85
94
|
}
|
|
86
95
|
/**
|
|
87
96
|
* Decode a uint16 field at a native pointer offset.
|
|
@@ -90,7 +99,7 @@ function decodeUint8At(ptr, offset) {
|
|
|
90
99
|
* @returns decoded value.
|
|
91
100
|
*/
|
|
92
101
|
function decodeUint16At(ptr, offset) {
|
|
93
|
-
return
|
|
102
|
+
return requireKoffi().decode(ptr, offset, "uint16");
|
|
94
103
|
}
|
|
95
104
|
/**
|
|
96
105
|
* Decode a uint32 field at a native pointer offset.
|
|
@@ -99,7 +108,7 @@ function decodeUint16At(ptr, offset) {
|
|
|
99
108
|
* @returns decoded value.
|
|
100
109
|
*/
|
|
101
110
|
function decodeUint32At(ptr, offset) {
|
|
102
|
-
return
|
|
111
|
+
return requireKoffi().decode(ptr, offset, "uint32");
|
|
103
112
|
}
|
|
104
113
|
/**
|
|
105
114
|
* Compare two in-memory SID records without allocating strings.
|
|
@@ -120,6 +129,8 @@ function sameSidAt(left, leftOffset, right, rightOffset) {
|
|
|
120
129
|
let cached;
|
|
121
130
|
function bindings() {
|
|
122
131
|
if (cached !== void 0) return cached;
|
|
132
|
+
const koffi = requireKoffi();
|
|
133
|
+
const { PVOID, PPVOID } = ffiTypes();
|
|
123
134
|
cached = extendWin32ProcessBindings(({ kernel32, advapi32, bind }) => ({
|
|
124
135
|
openProcess: bind(kernel32, "OpenProcess", PVOID, [
|
|
125
136
|
"uint32",
|
|
@@ -1041,13 +1052,15 @@ var AclSandbox = class {
|
|
|
1041
1052
|
const api = this.api;
|
|
1042
1053
|
const token = this.token;
|
|
1043
1054
|
if (api === void 0 || token === void 0) throw new Error("AclSandbox is not initialized: call init() first");
|
|
1055
|
+
if (options.controlFileDescriptor !== void 0 && options.stdio !== "inherit") throw new Error("control pipe requires inherited stdio");
|
|
1044
1056
|
const args = options.args ?? [];
|
|
1045
1057
|
const cwd = options.cwd ?? process.cwd();
|
|
1046
1058
|
if (options.stdio === "inherit") {
|
|
1047
1059
|
const native = spawnSandboxedInherited(api, token, {
|
|
1048
1060
|
command: options.command,
|
|
1049
1061
|
args,
|
|
1050
|
-
cwd
|
|
1062
|
+
cwd,
|
|
1063
|
+
...options.controlFileDescriptor === void 0 ? {} : { controlFileDescriptor: options.controlFileDescriptor }
|
|
1051
1064
|
});
|
|
1052
1065
|
let exitCodePromise;
|
|
1053
1066
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-sandbox-windows-acl",
|
|
3
3
|
"description": "Windows ACL write-restriction sandbox backend (restricted-token spawn with capability-SID write allowlist) for the DeepSeek Harness sandbox seam",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,15 +33,19 @@
|
|
|
33
33
|
],
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@deepseek-ai/cordis": "^4.0.2"
|
|
36
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
37
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.2"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"koffi": "^3.1.0",
|
|
40
|
-
"@deepseek-ai/dsh-
|
|
41
|
+
"@deepseek-ai/dsh-lazy-require": "^0.1.6-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-win32-process": "^0.1.6-alpha.2"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
|
-
"@deepseek-ai/dsh-pwsh-local": "^0.1.
|
|
44
|
-
"@deepseek-ai/dsh-sandbox-local": "^0.1.
|
|
45
|
-
"@deepseek-ai/cordis": "^4.0.2"
|
|
45
|
+
"@deepseek-ai/dsh-pwsh-local": "^0.1.6-alpha.2",
|
|
46
|
+
"@deepseek-ai/dsh-sandbox-local": "^0.1.6-alpha.2",
|
|
47
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
48
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.1.6-alpha.2"
|
|
46
50
|
}
|
|
47
51
|
}
|