@deepseek-ai/dsh-sandbox-windows-acl 0.0.1-rc.1

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.
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Lazy koffi bindings for the Win32 ACL-sandbox backend. Koffi loads lazily so
3
+ * non-Windows processes never open Win32 libraries. Every function signature
4
+ * below was verified against the MinGW Windows headers on this machine
5
+ * (winnt.h / accctrl.h / aclapi.h / securitybaseapi.h / sddl.h /
6
+ * processthreadsapi.h / fileapi.h / namedpipeapi.h / synchapi.h / winbase.h);
7
+ * struct layouts are asserted at load time against verify/abi-probe.cpp.
8
+ * @module @deepseek-ai/dsh-sandbox-windows-acl/ffi
9
+ */
10
+ /** Branded koffi 3 native pointer. Koffi 3 pointers are BigInt values; the brand keeps them out of numeric contexts. */
11
+ declare const nativePtr: unique symbol;
12
+ /** Koffi 3 native pointer (a BigInt address), branded so it cannot silently enter numeric contexts. */
13
+ export type NativePtr = bigint & {
14
+ readonly [nativePtr]: true;
15
+ };
16
+ /**
17
+ * True for NULL pointers, however koffi returns them (null or 0n).
18
+ * @param value - a pointer as koffi may hand it back (pointer, null, or 0n).
19
+ * @returns a type guard narrowing to the NULL shapes.
20
+ */
21
+ export declare function isNullPtr(value: NativePtr | null | undefined): value is null | undefined;
22
+ /**
23
+ * True for CreateFileW's INVALID_HANDLE_VALUE failure marker (-1, which
24
+ * koffi hands back as the unsigned 64-bit all-ones pointer).
25
+ * @param handle - the handle CreateFileW returned.
26
+ * @returns whether the handle signals failure.
27
+ */
28
+ export declare function isInvalidHandle(handle: NativePtr | null | undefined): boolean;
29
+ /** Field subset written into a zeroed STARTUPINFOW (layout verified: size 104). */
30
+ export interface StartupInfoInput {
31
+ cb: number;
32
+ dwFlags: number;
33
+ hStdInput: NativePtr;
34
+ hStdOutput: NativePtr;
35
+ hStdError: NativePtr;
36
+ }
37
+ /** Decoded PROCESS_INFORMATION (layout verified: size 24). */
38
+ export interface ProcessInfoOutput {
39
+ hProcess: NativePtr | null;
40
+ hThread: NativePtr | null;
41
+ dwProcessId: number;
42
+ dwThreadId: number;
43
+ }
44
+ /** The lazy koffi binding table: every Win32 call the ACL backend uses, signature-verified against the real headers. */
45
+ export interface Win32Bindings {
46
+ openProcess(desiredAccess: number, inheritHandle: number, pid: number): NativePtr;
47
+ openProcessToken(process: NativePtr, desiredAccess: number, tokenHandle: NativePtr): number;
48
+ closeHandle(handle: NativePtr): number;
49
+ getLastError(): number;
50
+ formatMessageW(flags: number, source: null, messageId: number, languageId: number, buffer: Buffer, size: number, args: null): number;
51
+ localAlloc(flags: number, bytes: number): NativePtr;
52
+ localFree(memory: NativePtr): NativePtr;
53
+ convertStringSidToSidW(stringSid: string, sid: NativePtr): number;
54
+ createWellKnownSid(type: number, domainSid: null, sid: NativePtr, size: NativePtr): number;
55
+ isValidSid(sid: NativePtr): number;
56
+ getLengthSid(sid: NativePtr): number;
57
+ copySid(length: number, destination: NativePtr, source: NativePtr): number;
58
+ getTokenInformation(token: NativePtr, cls: number, info: Buffer | null, length: number, needed: NativePtr): number;
59
+ setTokenInformation(token: NativePtr, cls: number, info: Buffer, length: number): number;
60
+ createRestrictedToken(existing: NativePtr, flags: number, disableCount: number, disableSids: null, deletePrivilegeCount: number, privilegesToDelete: null, restrictCount: number, restrictingSids: Buffer, newToken: NativePtr): number;
61
+ setEntriesInAclW(count: number, entries: Buffer, oldAcl: NativePtr | null, newAcl: NativePtr): number;
62
+ setNamedSecurityInfoW(path: string, objectType: number, information: number, owner: null, group: null, dacl: NativePtr | null, sacl: null): number;
63
+ getNamedSecurityInfoW(path: string, objectType: number, information: number, owner: NativePtr, group: NativePtr, dacl: NativePtr, sacl: NativePtr, descriptor: NativePtr): number;
64
+ getTempPathW(length: number, buffer: Buffer): number;
65
+ createFileW(fileName: string, desiredAccess: number, shareMode: number, attributes: null, creationDisposition: number, flagsAndAttributes: number, templateFile: null): NativePtr;
66
+ lockFileEx(file: NativePtr, flags: number, reserved: number, bytesLow: number, bytesHigh: number, overlapped: NativePtr): number;
67
+ unlockFileEx(file: NativePtr, reserved: number, bytesLow: number, bytesHigh: number, overlapped: NativePtr): number;
68
+ createPipe(readHandle: NativePtr, writeHandle: NativePtr, attributes: null, size: number): number;
69
+ setHandleInformation(handle: NativePtr, mask: number, flags: number): number;
70
+ createProcessAsUserW(token: NativePtr, applicationName: null, commandLine: string, processAttributes: null, threadAttributes: null, inheritHandles: number, creationFlags: number, environment: null, currentDirectory: string | null, startupInfo: NativePtr, processInfo: NativePtr): number;
71
+ setEnvironmentVariableW(name: string, value: string): number;
72
+ readFile(file: NativePtr, buffer: Buffer, count: number, bytesRead: NativePtr, overlapped: null): number;
73
+ peekNamedPipe(pipe: NativePtr, buffer: null, size: number, bytesRead: NativePtr, totalAvail: NativePtr, leftThisMessage: NativePtr): number;
74
+ waitForSingleObject(handle: NativePtr, milliseconds: number): number;
75
+ getExitCodeProcess(process: NativePtr, exitCode: NativePtr): number;
76
+ resumeThread(thread: NativePtr): number;
77
+ createJobObjectW(attributes: null, name: null): NativePtr;
78
+ setInformationJobObject(job: NativePtr, cls: number, information: Buffer, length: number): number;
79
+ assignProcessToJobObject(job: NativePtr, process: NativePtr): number;
80
+ terminateProcess(process: NativePtr, exitCode: number): number;
81
+ setConsoleCtrlHandler(handler: null, add: number): number;
82
+ getStdHandle(stdHandle: number): NativePtr;
83
+ }
84
+ /** koffi STARTUPINFOW layout; its size is asserted against abi.STARTUPINFOW_SIZE at load. */
85
+ export declare const STARTUPINFOW: import("koffi").TypeObject;
86
+ /** koffi PROCESS_INFORMATION layout; its size is asserted against abi.PROCESS_INFORMATION_SIZE at load. */
87
+ export declare const PROCESS_INFORMATION: import("koffi").TypeObject;
88
+ /**
89
+ * Allocate one pointer-sized slot (for `T **` out-parameters).
90
+ * @returns the allocated slot pointer.
91
+ */
92
+ export declare function allocPtrSlot(): NativePtr;
93
+ /**
94
+ * Allocate one uint32 slot.
95
+ * @returns the allocated slot pointer.
96
+ */
97
+ export declare function allocUint32(): NativePtr;
98
+ /**
99
+ * Write a uint32 value into a slot pointer.
100
+ * @param slot - the slot allocated by {@link allocUint32}.
101
+ * @param value - the uint32 to encode.
102
+ */
103
+ export declare function encodeUint32(slot: NativePtr, value: number): void;
104
+ /**
105
+ * Decode the pointer stored in a pointer-sized slot (NULL becomes null).
106
+ * @param slot - the pointer-sized slot holding the out-parameter value.
107
+ * @returns the decoded pointer, or null for NULL.
108
+ */
109
+ export declare function decodePtr(slot: NativePtr): NativePtr | null;
110
+ /**
111
+ * Decode a uint32 at a slot pointer.
112
+ * @param slot - the uint32 slot holding the out-parameter value.
113
+ * @returns the decoded uint32.
114
+ */
115
+ export declare function decodeUint32(slot: NativePtr): number;
116
+ /**
117
+ * Cast a koffi pointer to its numeric address (bigint, used for raw struct packing).
118
+ * @param ptr - the koffi pointer.
119
+ * @returns the pointer's numeric address.
120
+ */
121
+ export declare function ptrAddress(ptr: NativePtr): bigint;
122
+ /**
123
+ * Allocate a raw byte block (used for SID copies and variable-length arrays).
124
+ * @param length - the block size in bytes.
125
+ * @returns the allocated block pointer.
126
+ */
127
+ export declare function allocBytes(length: number): NativePtr;
128
+ /**
129
+ * Allocate one zeroed OVERLAPPED (32 bytes on x64: Internal@0, InternalHigh@8,
130
+ * Offset@16, OffsetHigh@20, hEvent@24). LockFileEx/UnlockFileEx receive this
131
+ * instead of a NULL lpOverlapped: koffi 3.1.1 crashes on NULL there, and a
132
+ * zeroed OVERLAPPED on a synchronous file handle is the documented equivalent
133
+ * (the byte range locks from offset 0, hEvent stays NULL).
134
+ * @returns the zeroed block pointer.
135
+ */
136
+ export declare function allocOverlapped(): NativePtr;
137
+ /**
138
+ * Decode a pointer VALUE stored in memory at `buffer[offset]` (e.g. TOKEN_GROUPS entries).
139
+ * @param buffer - the buffer holding the pointer value.
140
+ * @param offset - byte offset of the pointer inside the buffer.
141
+ * @returns the decoded pointer, or null for NULL.
142
+ */
143
+ export declare function decodePtrAt(buffer: Buffer, offset: number): NativePtr | null;
144
+ /**
145
+ * Decode a uint8 at a native pointer plus byte offset — the ACL walk's
146
+ * field-read primitive (koffi.decode with an offset, no memcpy, no pointer
147
+ * arithmetic).
148
+ * @param ptr - the native pointer to read from.
149
+ * @param offset - byte offset from the pointer.
150
+ * @returns the decoded uint8.
151
+ */
152
+ export declare function decodeUint8At(ptr: NativePtr, offset: number): number;
153
+ /**
154
+ * Decode a uint16 at a native pointer plus byte offset (see {@link decodeUint8At}).
155
+ * @param ptr - the native pointer to read from.
156
+ * @param offset - byte offset from the pointer.
157
+ * @returns the decoded uint16.
158
+ */
159
+ export declare function decodeUint16At(ptr: NativePtr, offset: number): number;
160
+ /**
161
+ * Decode a uint32 at a native pointer plus byte offset (see {@link decodeUint8At}).
162
+ * @param ptr - the native pointer to read from.
163
+ * @param offset - byte offset from the pointer.
164
+ * @returns the decoded uint32.
165
+ */
166
+ export declare function decodeUint32At(ptr: NativePtr, offset: number): number;
167
+ /**
168
+ * Compare two SIDs field-by-field via BOUNDED offset reads (revision, count,
169
+ * identifier authority, subauthorities up to the count) — never a fixed-size
170
+ * struct decode, which would read past a short SID allocation (a SID with
171
+ * fewer than 8 subauthorities is smaller than `SID_STRUCT`). An implausible
172
+ * subauthority count reads as unequal.
173
+ * @param left - pointer to one SID (offset 0).
174
+ * @param leftOffset - byte offset of the SID structure within `left`.
175
+ * @param right - pointer to the other SID.
176
+ * @param rightOffset - byte offset of the SID structure within `right`.
177
+ * @returns whether the SIDs are identical.
178
+ */
179
+ export declare function sameSidAt(left: NativePtr, leftOffset: number, right: NativePtr, rightOffset: number): boolean;
180
+ /**
181
+ * Allocate a zeroed STARTUPINFOW.
182
+ * @returns the allocated struct pointer.
183
+ */
184
+ export declare function allocStartupInfo(): NativePtr;
185
+ /**
186
+ * Write the stdio-relevant fields into a zeroed STARTUPINFOW (others stay default-initialized).
187
+ * @param startupInfo - the allocated STARTUPINFOW to encode into.
188
+ * @param fields - the field subset to write.
189
+ */
190
+ export declare function encodeStartupInfo(startupInfo: NativePtr, fields: StartupInfoInput): void;
191
+ /**
192
+ * Allocate a zeroed PROCESS_INFORMATION.
193
+ * @returns the allocated struct pointer.
194
+ */
195
+ export declare function allocProcessInfo(): NativePtr;
196
+ /**
197
+ * Decode a PROCESS_INFORMATION after CreateProcessAsUserW.
198
+ * @param processInfo - the PROCESS_INFORMATION filled by the spawn call.
199
+ * @returns the decoded handle/id fields.
200
+ */
201
+ export declare function decodeProcessInfo(processInfo: NativePtr): ProcessInfoOutput;
202
+ /**
203
+ * Resolve the lazy Win32 bindings (throws the first binding failure, fail-closed).
204
+ * @returns the cached binding table.
205
+ */
206
+ export declare function win32(): Promise<Win32Bindings>;
207
+ /**
208
+ * Resolve the lazy Win32 bindings SYNCHRONOUSLY — the sandbox seam's
209
+ * server-side per-session grant materializes ACEs inside the synchronous
210
+ * `confine()` call, which cannot await. Same cached table as {@link win32}
211
+ * (the underlying koffi loads are synchronous; the async wrapper exists for
212
+ * the runner's await-shaped call sites).
213
+ * @returns the cached binding table.
214
+ */
215
+ export declare function win32Sync(): Win32Bindings;
216
+ /**
217
+ * Turn a Win32 error code into readable text via FormatMessageW.
218
+ * @param api - the binding table.
219
+ * @param win32Code - the error code to format.
220
+ * @returns the formatted message text, or '' when formatting fails.
221
+ */
222
+ export declare function errorText(api: Win32Bindings, win32Code: number): string;
223
+ /**
224
+ * Read the process temp directory via GetTempPathW (fileapi.h line ~188).
225
+ * Defensive against an overlong system temp path: GetTempPathW reports the
226
+ * REQUIRED length (including NUL) without writing the buffer when it is too
227
+ * small, so a reported length beyond the buffer's capacity means the buffer
228
+ * was never filled and must not be decoded.
229
+ * @param api - the binding table.
230
+ * @returns the NUL-terminated temp path decoded as a string.
231
+ */
232
+ export declare function getTempPath(api: Win32Bindings): string;
233
+ /**
234
+ * Throw a Win32Error for a BOOL-style API failure. MUST be called immediately
235
+ * after the failed call so GetLastError is not clobbered by other Win32 calls.
236
+ * @param api - the binding table.
237
+ * @param name - the failed API's name for the error message.
238
+ * @param detail - optional detail overriding the formatted system message.
239
+ * @returns never — always throws.
240
+ */
241
+ export declare function throwLastError(api: Win32Bindings, name: string, detail?: string): never;
242
+ /**
243
+ * Throw a Win32Error for an HRESULT-style API return value (the value IS the error code).
244
+ * @param api - the binding table.
245
+ * @param name - the failed API's name for the error message.
246
+ * @param win32Code - the API's returned error code.
247
+ * @param detail - optional detail overriding the formatted system message.
248
+ * @returns never — always throws.
249
+ */
250
+ export declare function throwWin32(api: Win32Bindings, name: string, win32Code: number, detail?: string): never;
251
+ export {};
252
+ //# sourceMappingURL=ffi.d.ts.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Server-side per-session write grant: the ACE materialization half of the
3
+ * sandbox seam's per-session grant reuse. The seam (sandbox-local) holds ONE
4
+ * {@link AclWriteGrant} per session for the server process's lifetime —
5
+ * created lazily at the session's first confined execution, reused (never
6
+ * re-applied) for every later call, revoked on provider dispose. The durable
7
+ * half (the session's SID and paths surviving a restart) lives in the
8
+ * session log, owned by the seam; this module owns only the native half: the
9
+ * parsed SID pointer and the standing ACEs.
10
+ *
11
+ * Fail-closed: `add` throws on any grant failure and the caller disposes the
12
+ * instance (revoking every path granted so far); `dispose` revokes every
13
+ * standing grant and reports every cleanup failure.
14
+ * @module @deepseek-ai/dsh-sandbox-windows-acl/grant
15
+ */
16
+ import type { Win32Bindings } from './ffi.ts';
17
+ /**
18
+ * One write SID's server-lifetime grant materialization: the parsed SID
19
+ * pointer plus every directory whose DACL currently carries its ACE.
20
+ * Workspace paths are added STANDING (their ACEs are the cross-session reuse
21
+ * cache and outlive the grant — dispose() skips revoking them, or the next
22
+ * provision would re-propagate the whole tree); temp paths are revocable
23
+ * (dispose() revokes them — an inheritable ACE must not outlive its
24
+ * session's temp directory). Create with {@link AclWriteGrant.create};
25
+ * dispose revokes the revocable paths and frees the SID.
26
+ */
27
+ export declare class AclWriteGrant {
28
+ /** The write SID in SDDL string form. */
29
+ readonly writeSid: string;
30
+ private readonly api;
31
+ private readonly sidPtr;
32
+ private readonly revocablePaths;
33
+ private readonly standingPaths;
34
+ private constructor();
35
+ /**
36
+ * Parse the SID string and open the binding table (lazily, once per
37
+ * server). Fail-closed: any failure throws — nothing is granted yet.
38
+ * @param writeSid - the orphan write SID string (`S-1-4-x-y`).
39
+ * @param api - optional already-resolved bindings (tests).
40
+ * @returns the ready grant (no ACEs yet).
41
+ */
42
+ static create(writeSid: string, api?: Win32Bindings): AclWriteGrant;
43
+ /**
44
+ * Grant the write ACE on one directory (idempotent: an already-standing
45
+ * exact ACE skips the eager full-tree re-propagation — see
46
+ * {@link grantWrite}) and record the path for {@link dispose} unless it is
47
+ * standing. The path is recorded BEFORE the grant: a post-apply throw (a
48
+ * LocalFree failure after SetNamedSecurityInfoW succeeded) must still
49
+ * revoke it, and revoking an ungranted path is a no-op merge. Callers
50
+ * treat a throw as a failed materialization and dispose the instance to
51
+ * revoke the paths granted so far.
52
+ * @param path - the directory whose DACL gains the grant.
53
+ * @param standing - the ACE outlives this grant (the workspace reuse
54
+ * cache; dispose() skips revoking it). Default false (revoked on
55
+ * dispose — the temp-directory lifecycle).
56
+ */
57
+ add(path: string, standing?: boolean): void;
58
+ /** Every directory currently carrying the grant, in grant order. */
59
+ get paths(): readonly string[];
60
+ /** Revoke every revocable grant (standing ACEs stay) and free the SID; reports every cleanup failure. */
61
+ dispose(): void;
62
+ }
63
+ //# sourceMappingURL=grant.d.ts.map
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Windows ACL write-restriction sandbox backend for the DeepSeek Harness
3
+ * sandbox seam. Mirrors the mechanism of github.com/huoyaoyuan/
4
+ * windows-acl-restrict-poc @ 10e4dfb (the fixed revision): a WRITE_RESTRICTED
5
+ * token whose restricting SIDs include a write SID (`S-1-4-x-y`) that only
6
+ * this sandbox adds to the target directories' DACLs — the intersection
7
+ * check then allows writes exactly where that SID has a Write ACE, and
8
+ * nowhere else the write SID is concerned (the token's write check ALSO
9
+ * inherits the ambient write ACEs of the other restricting SIDs — the
10
+ * keep-alive group logon SID + Everyone; Authenticated Users, INTERACTIVE,
11
+ * and LOCAL are absent from both lists — see the seam's dual-list contract
12
+ * in `packages/sandbox/sandbox-local` and the package README's Modes section
13
+ * for the complete boundary). The write SID is the per-WORKSPACE identity
14
+ * ({@link workspaceWriteSid}): deterministic from the canonical workspace
15
+ * path, so the workspace-root ACE materializes once per workspace per
16
+ * machine and every later provision hits the exact-ACE skip — the
17
+ * grant-reuse story the per-session random SID paid a full tree propagation
18
+ * per session for. Unlike the POC, every API failure throws with the API
19
+ * name and exact Win32 code; a child is NEVER spawned unrestricted.
20
+ *
21
+ * Known boundaries (inherent to restricted tokens, not this port):
22
+ * - writes are restricted; reads, network, and process visibility are NOT
23
+ * (WRITE_RESTRICTED intersects only write accesses);
24
+ * - console isolation is unavailable — children share the host console
25
+ * (CREATE_NO_WINDOW / CREATE_NEW_CONSOLE children die with
26
+ * STATUS_DLL_INIT_FAILED under the restriction);
27
+ * - the temp directory and every writable directory must be owned by the
28
+ * caller (owner-implicit WRITE_DAC);
29
+ * - grants are standing ACE mutations on real directories. WORKSPACE grants
30
+ * are deliberately never revoked — the ACE is the cross-session reuse
31
+ * cache (revoking would force the next session to re-propagate the whole
32
+ * tree). TEMP grants are revocable: dispose() removes them so a standing
33
+ * inheritable ACE never outlives its session's temp directory (an
34
+ * inheritable ACE on the ambient temp root would otherwise widen the
35
+ * SID's write reach to every future temp file). With `manageDacls: false`
36
+ * the CALLER owns the DACLs (the sandbox seam's grant reuse):
37
+ * init()/dispose() skip grant/revoke entirely and the caller must not
38
+ * revoke under live children.
39
+ * @module @deepseek-ai/dsh-sandbox-windows-acl
40
+ */
41
+ export { quoteArg } from './spawn.ts';
42
+ export { AclWriteGrant } from './grant.ts';
43
+ export { workspaceWriteSid } from './workspace-sid.ts';
44
+ export { Win32Error } from './errors.ts';
45
+ /** Construction options: the write allowlist, the optional temp grant, and the orphan SID identity. */
46
+ export interface AclSandboxOptions {
47
+ /** Directories the confined child may write into (must exist and be caller-owned). */
48
+ writableDirs: readonly string[];
49
+ /**
50
+ * Temp directory to also grant; defaults to GetTempPathW() at init time.
51
+ * Pass null for read-only confinement: NO temp grant (strict zero grant on
52
+ * the filesystem; the NUL device stays ambient-writable via Everyone — see
53
+ * README).
54
+ */
55
+ tempDir?: string | null;
56
+ /**
57
+ * The write SID forming the workspace-write allowlist: REQUIRED under
58
+ * workspace-write, ignored (and must be absent) under read-only. Callers
59
+ * derive it from the workspace via {@link workspaceWriteSid} — the identity
60
+ * is per workspace, not per sandbox instance, so the workspace-root ACE
61
+ * outlives every instance and later provisions hit the exact-ACE skip.
62
+ */
63
+ writeSid?: string;
64
+ /**
65
+ * The file-effect mode this instance confines under — selects the
66
+ * restricted token's restricting-SID list (I for read-only, J for
67
+ * workspace-write) and MUST match the grant shape: read-only pairs with
68
+ * zero grants. The runner validates the argv-borne mode string at its
69
+ * boundary; this typed seam trusts the union.
70
+ */
71
+ mode: 'read-only' | 'workspace-write';
72
+ /**
73
+ * Whether this instance owns its DACL grants (default true). False means
74
+ * the CALLER has already materialized the ACEs (the sandbox seam's
75
+ * per-session grant reuse): init()/dispose() skip grant/revoke entirely —
76
+ * the caller holds the grants for its own lifetime and revokes them.
77
+ */
78
+ manageDacls?: boolean;
79
+ }
80
+ /** Per-spawn options: the program, its argv/cwd, and the stdio shape. */
81
+ export interface AclSandboxSpawnOptions {
82
+ /** Program to run (resolved via PATH search when unqualified, like CreateProcess). */
83
+ command: string;
84
+ /** Arguments, quoted per CommandLineToArgvW rules. */
85
+ args?: readonly string[];
86
+ /** Working directory; defaults to the caller's cwd. */
87
+ cwd?: string;
88
+ /**
89
+ * 'pipe' (default): capture stdout/stderr via anonymous pipes.
90
+ * 'inherit': the child inherits the caller's stdio directly (runner usage —
91
+ * bytes flow straight through), always wrapped in a kill-on-close job so the
92
+ * child dies with the caller; stdout/stderr in the result are empty.
93
+ */
94
+ stdio?: 'pipe' | 'inherit';
95
+ }
96
+ /** A settled confined child: captured stdio and the exit code. */
97
+ export interface AclSandboxChildResult {
98
+ stdout: Buffer;
99
+ stderr: Buffer;
100
+ exitCode: number;
101
+ }
102
+ /** A running confined child: its pid and a settlement promise. */
103
+ export interface AclSandboxChild {
104
+ /** Child process id. */
105
+ pid: number;
106
+ /** Resolve stdout/stderr and the exit code once the child exits. */
107
+ wait(): Promise<AclSandboxChildResult>;
108
+ }
109
+ /**
110
+ * One write-restricted sandbox instance: token + write-SID grants + spawn.
111
+ * `init()` is fail-closed — any Win32 failure revokes the revocable (temp)
112
+ * grants and throws; `dispose()` revokes the temp grants, leaves the
113
+ * standing workspace ACEs in place (the cross-instance reuse cache), frees
114
+ * every allocation, and reports every cleanup failure. With
115
+ * `manageDacls: false` the caller owns the grants (the sandbox seam's grant
116
+ * reuse): init() applies none and dispose() revokes none.
117
+ */
118
+ export declare class AclSandbox {
119
+ /** Absolute writable directories (constructor-validated). */
120
+ readonly writableDirs: string[];
121
+ /** The write SID string whose ACEs form the write allowlist (workspace-write only). */
122
+ readonly writeSid: string | undefined;
123
+ /** The file-effect mode — the restricted token's restricting-SID list selection. */
124
+ readonly mode: 'read-only' | 'workspace-write';
125
+ private readonly tempDirOption;
126
+ private readonly manageDacls;
127
+ private tempDirResolved;
128
+ private api;
129
+ private token;
130
+ private writeSidPtr;
131
+ /** The well-known/logon SID allocations init() makes; freed by dispose() alongside the write SID. */
132
+ private sidAllocations;
133
+ private grantedPaths;
134
+ constructor(options: AclSandboxOptions);
135
+ /** Resolved temp directory (available after init; null when temp grants are disabled). */
136
+ get tempDir(): string | null | undefined;
137
+ /** Create the restricted token and apply the orphan-SID grants. Idempotent-unsafe: once per instance. */
138
+ init(): Promise<void>;
139
+ /**
140
+ * Spawn a process under the restricted token. Fails closed: throws on every
141
+ * Win32 failure; the child is never created unrestricted. With
142
+ * `stdio: 'inherit'` the child shares the caller's stdio directly and is
143
+ * placed in a kill-on-close job (dies with the caller). Call dispose() only
144
+ * after all children have exited — revoking grants under a live child
145
+ * removes its remaining write allowance.
146
+ * @param options - the program, argv/cwd, and stdio shape.
147
+ * @returns the running child.
148
+ */
149
+ spawn(options: AclSandboxSpawnOptions): AclSandboxChild;
150
+ /**
151
+ * Revoke the revocable (temp) grants, free the SID, close the token; the
152
+ * standing workspace ACEs stay (the reuse cache). Reports every cleanup
153
+ * failure.
154
+ */
155
+ dispose(): void;
156
+ }
157
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-sandbox-windows-acl`.
3
+ * @module @deepseek-ai/dsh-sandbox-windows-acl/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "sandbox-windows-acl-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The windows-acl confinement runner: the argv-prefix wrapper the sandbox
3
+ * seam spawns in place of the caller's command. It creates the
4
+ * WRITE_RESTRICTED token with the workspace write-SID allowlist, spawns the
5
+ * wrapped argv under it with the CALLER'S stdio inherited (bytes flow
6
+ * straight through), mirrors the child's exit code, and revokes its temp
7
+ * grant on exit (workspace ACEs stay standing as the reuse cache).
8
+ *
9
+ * Stable argv contract (the seam builds it; a native-exe replacement would
10
+ * keep the same contract):
11
+ * [node, runner.js, '--workspace', <dir>, '--temp', <dir>,
12
+ * '--mode', <read-only|workspace-write>,
13
+ * ['--write-sid', <S-1-4-…>], '--', <argv...>]
14
+ *
15
+ * Modes:
16
+ * - workspace-write: the workspace and temp directories carry the orphan-SID
17
+ * Write grant; every other write is denied by the token intersection.
18
+ * - read-only: STRICT zero grants — no directory is writable, not even the
19
+ * NUL device (`> $null` fails with access denied); the restricting list
20
+ * carries no orphan SID, so a standing grant ACE from an earlier
21
+ * workspace-write period stays inert. BOTH modes drop Authenticated Users
22
+ * (CIM unavailable — documented in README) and INTERACTIVE/LOCAL (the
23
+ * Public tree writes are denied); the two lists share the keep-alive group
24
+ * (logon SID, EVERYONE) and differ only by the orphan.
25
+ *
26
+ * `--write-sid`: the seam's grant contract — the CALLER has already
27
+ * materialized the write-SID ACEs (the seam's workspace + private-temp
28
+ * grants, server lifetime) and owns their revocation, so the runner neither
29
+ * grants nor revokes (manageDacls: false). The carried SID is the
30
+ * per-workspace identity ({@link workspaceWriteSid}) — the seam derives it
31
+ * from the policy root; the flag's PRESENCE is the seam-managed marker (its
32
+ * value must equal the workspace-derived SID). Absent `--write-sid`
33
+ * (standalone/test use) the runner self-manages grants per invocation with
34
+ * the same workspace-derived SID (its workspace ACEs are standing — the
35
+ * reuse cache — and its temp ACE is revoked on exit). With `--write-sid` in
36
+ * workspace-write mode, the runner rewrites the TMP/TEMP entries of its OWN
37
+ * environment (SetEnvironmentVariableW) to the `--temp` directory — a
38
+ * PRIVATE per-session temp subdirectory the seam provisions (bwrap `--tmpfs
39
+ * /tmp` semantics) — and the child inherits the rewritten block (lpEnvironment
40
+ * NULL; an explicit block through koffi trips ERROR_INVALID_PARAMETER in
41
+ * CreateProcessAsUserW, verified empirically). Read-only leaves the ambient
42
+ * temp entries untouched (writes there are denied anyway).
43
+ *
44
+ * Failure contract: every runner-side failure (bad args, missing
45
+ * directories, token/grant/spawn errors) prints `windows-acl-run: <detail>`
46
+ * to stderr and exits 127 — the seam's RUNNER_FAILURE_RULES matches that
47
+ * signature. The child is NEVER spawned unrestricted.
48
+ * @module @deepseek-ai/dsh-sandbox-windows-acl/runner
49
+ */
50
+ export {};
51
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Restricted-process spawning: anonymous pipes for stdio, STARTUPINFOW with
3
+ * STARTF_USESTDHANDLES, CreateProcessAsUserW under the restricted token, then
4
+ * asynchronous pipe draining and exit waiting. Console isolation
5
+ * (CREATE_NO_WINDOW / CREATE_NEW_CONSOLE) is intentionally absent: under this
6
+ * restriction scheme hidden-console children die with STATUS_DLL_INIT_FAILED
7
+ * (0xC0000142) — verified empirically, see win32-abi.ts. Stdio redirection is
8
+ * pipe-based and unaffected; the child shares the host console.
9
+ * @module @deepseek-ai/dsh-sandbox-windows-acl/spawn
10
+ */
11
+ import type { NativePtr, Win32Bindings } from './ffi.ts';
12
+ /**
13
+ * Quote one argument per the CommandLineToArgvW parsing rules: backslashes
14
+ * are doubled only before a quote character — including the closing quote
15
+ * this function appends, so a trailing backslash run is doubled as well
16
+ * (otherwise an odd run would escape the closing quote into a literal
17
+ * character and corrupt the rest of the command line). Mirrors the CRT
18
+ * ArgvQuote behavior Microsoft documents for command-line arguments.
19
+ * @param argument - one argv entry to quote.
20
+ * @returns the quoted entry (bare when quoting is unnecessary).
21
+ */
22
+ export declare function quoteArg(argument: string): string;
23
+ /**
24
+ * Build the single command line CreateProcess parses from program + argv.
25
+ * @param program - the executable (argv[0]).
26
+ * @param args - the remaining argv entries.
27
+ * @returns the joined, quoted command line.
28
+ */
29
+ export declare function buildCommandLine(program: string, args: readonly string[]): string;
30
+ /** A confined child spawned with piped stdio: process handle plus the pipe read ends to drain. */
31
+ export interface SpawnedNative {
32
+ pid: number;
33
+ process: NativePtr;
34
+ stdoutRead: NativePtr;
35
+ stderrRead: NativePtr;
36
+ }
37
+ /**
38
+ * Create a process under the restricted token with piped stdio. The child's
39
+ * stdin is closed immediately (EOF), matching the POC; stdout/stderr read ends
40
+ * are returned for draining. The child inherits the caller's environment block
41
+ * (lpEnvironment NULL); the caller rewrites entries through
42
+ * SetEnvironmentVariableW before spawning (the runner's per-session temp
43
+ * contract) — passing an explicit block through koffi trips
44
+ * ERROR_INVALID_PARAMETER in CreateProcessAsUserW (verified empirically).
45
+ * @param api - the binding table.
46
+ * @param token - the restricted token the child runs under.
47
+ * @param options - command, args, and working directory.
48
+ * @returns the spawned child's handles.
49
+ */
50
+ export declare function spawnSandboxed(api: Win32Bindings, token: NativePtr, options: {
51
+ command: string;
52
+ args: readonly string[];
53
+ cwd: string;
54
+ }): SpawnedNative;
55
+ /**
56
+ * Drain one pipe read end to a Buffer via non-blocking PeekNamedPipe polling.
57
+ * @param api - the binding table.
58
+ * @param handle - the pipe read end to drain (closed when done).
59
+ * @returns the complete pipe contents.
60
+ */
61
+ export declare function drainPipe(api: Win32Bindings, handle: NativePtr): Promise<Buffer>;
62
+ /**
63
+ * Wait for process exit and return its exit code. Call only after both drains
64
+ * have resolved — the drains finish when the child closed its pipe ends, i.e.
65
+ * the child has already exited, so this wait returns immediately. Calling it
66
+ * earlier would block the event loop and starve the drains (the pipe-buffer
67
+ * deadlock the POC comments warn about).
68
+ * @param api - the binding table.
69
+ * @param process - the child process handle (closed when done).
70
+ * @returns the child's exit code.
71
+ */
72
+ export declare function waitForExit(api: Win32Bindings, process: NativePtr): number;
73
+ /** A confined child spawned with inherited stdio: process handle plus its kill-on-close job. */
74
+ export interface SpawnedInherited {
75
+ pid: number;
76
+ process: NativePtr;
77
+ /** Kill-on-close job the child was placed in; caller closes it after the child exits. */
78
+ job: NativePtr;
79
+ }
80
+ /**
81
+ * Create a process under the restricted token whose stdio passes straight
82
+ * through to the caller's pipes. This is the runner shape: the harness spawns
83
+ * the runner with piped stdio, and the runner's confined child writes to
84
+ * those same pipes.
85
+ *
86
+ * Node clears the inheritability of its stdio handles at startup
87
+ * (uv_disable_stdio_inheritance), so raw spawns must re-enable the inherit
88
+ * bit around the call (libuv instead duplicates the handles; re-enabling is
89
+ * equivalent here and cheaper) and pass them explicitly via
90
+ * STARTF_USESTDHANDLES — otherwise the child receives INVALID std handles
91
+ * ("The handle is invalid", verified the hard way). The child starts
92
+ * suspended so it can be assigned to a kill-on-close job before it runs.
93
+ * @param api - the binding table.
94
+ * @param token - the restricted token the child runs under.
95
+ * @param options - command, args, and working directory.
96
+ * @returns the spawned child's handles and job.
97
+ */
98
+ export declare function spawnSandboxedInherited(api: Win32Bindings, token: NativePtr, options: {
99
+ command: string;
100
+ args: readonly string[];
101
+ cwd: string;
102
+ }): SpawnedInherited;
103
+ //# sourceMappingURL=spawn.d.ts.map