docs(sandbox): satisfy the doc-sync gates for the Windows ACL packages
JSDoc every sandbox-windows-acl export (the 138 verify-export-jsdoc violations); fix the sandbox-windows-acl README code block (undeclared workspaceRoot), bring its stale claims current (the provider wiring and kill-on-close are live), add the canonical Model Experience sections to both new packages, audited the backend's indirect Model Experience sentence, and regenerate docs/config-catalog.md.
This commit is contained in:
@@ -1100,6 +1100,26 @@ export interface Config {
|
||||
|
||||
Source: [`packages/bash/pwsh-local/src/index.ts:54`](../packages/bash/pwsh-local/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-pwsh-sandbox`
|
||||
|
||||
Requires: `subprocess` · `sandbox` · `sandboxPolicy`
|
||||
|
||||
```ts config-catalog
|
||||
/**
|
||||
* Plugin config: the local executor's knobs, verbatim. The sandbox policy —
|
||||
* the default mode and fallback `workspace-write` root — is NOT here: it lives
|
||||
* on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), which resolves
|
||||
* each calling session's mode and cwd for every enforcing capability. The
|
||||
* runner choice is likewise the `ctx.sandbox` provider's config, not this
|
||||
* executor's.
|
||||
*/
|
||||
export type Config = LocalConfig
|
||||
```
|
||||
|
||||
Depends on: [`LocalConfig`](#deepseek-aidsh-pwsh-local)
|
||||
|
||||
Source: [`packages/bash/pwsh-sandbox/src/index.ts:39`](../packages/bash/pwsh-sandbox/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-repeat-tool-guard`
|
||||
|
||||
```ts config-catalog
|
||||
@@ -1175,7 +1195,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/sandbox/sandbox-local/src/index.ts:24`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
Source: [`packages/sandbox/sandbox-local/src/index.ts:28`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-sandbox-policy`
|
||||
|
||||
@@ -2611,6 +2631,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
|
||||
- `@deepseek-ai/dsh-native-command` ([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
|
||||
- `@deepseek-ai/dsh-paths` ([`packages/util/paths/src/index.ts`](../packages/util/paths/src/index.ts))
|
||||
- `@deepseek-ai/dsh-retention` ([`packages/util/retention/src/index.ts`](../packages/util/retention/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sandbox-windows-acl` ([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
|
||||
- `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
|
||||
- `@deepseek-ai/dsh-scripts` ([`packages/sdk/scripts/src/index.ts`](../packages/sdk/scripts/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-client` ([`packages/sdk/sdk-client/src/index.ts`](../packages/sdk/sdk-client/src/index.ts))
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/bash/pwsh-sandbox/README.md
|
||||
README.md: 222c61176b71042bfcb539a839507a425a1fb9e5
|
||||
README.zh.md: 02326bc36c9a7e1fe41817b9ab782e31a6d0b9b9
|
||||
README.md: bd506d011fa6167ddf7d6fe0565e475979ad0ec2
|
||||
README.zh.md: e9aa380302037be3c9dd07331035544299bf3bec
|
||||
|
||||
@@ -15,9 +15,19 @@ The executor inherits [`@deepseek-ai/dsh-pwsh-local`](../pwsh-local/)'s process
|
||||
|
||||
### Confinement works, denial surfaces as command failure
|
||||
|
||||
The model sees the confined command's own stderr (e.g. `Access to the path '...' is denied.` under the Windows ACL runner); the tool layer converts classified denials into the standard permission-denied surface exactly as it does for the bash tool.
|
||||
#### What the model sees
|
||||
|
||||
## Known Limitations
|
||||
The confined command's own stderr (e.g. `Access to the path '...' is denied.` under the Windows ACL runner); the tool layer converts classified denials into the standard permission-denied surface exactly as it does for the bash tool.
|
||||
|
||||
#### Token effect
|
||||
|
||||
No model-visible text beyond the command's stderr and the tool layer's standard denial surface.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None directly; the denial surface belongs to the tool layer.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Reads are unrestricted** on Windows (the ACL runner restricts writes only); the read boundary is documented in `@deepseek-ai/dsh-sandbox-windows-acl`.
|
||||
- **The Windows workspace-write temp area is the real temp directory** (`GetTempPathW`). This is a deliberate backend-defined choice, the same decision Landlock makes (`readWrite: ['/tmp', ...]`): the seam's "backend-defined temp area" permits it, and the escape probe in `tests/acl.e2e.ts` lives outside the temp tree for exactly that reason. A per-run private temp (bwrap's `--tmpfs /tmp` semantics) would additionally need an environment-block rewrite in the runner; it is an optional future hardening, not a correctness gap.
|
||||
|
||||
@@ -15,9 +15,19 @@
|
||||
|
||||
### 隔离生效,拒绝以命令失败呈现
|
||||
|
||||
模型看到受限命令自身的 stderr(Windows ACL runner 下如 `Access to the path '...' is denied.`);工具层把分类后的拒绝转成标准权限拒绝面,与 bash 工具完全一致。
|
||||
#### 模型看到什么
|
||||
|
||||
## 已知限制
|
||||
受限命令自身的 stderr(Windows ACL runner 下如 `Access to the path '...' is denied.`);工具层把分类后的拒绝转成标准权限拒绝面,与 bash 工具完全一致。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
除命令 stderr 与工具层标准拒绝面外,无额外模型可见文本。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无直接影响;拒绝呈现面属于工具层。
|
||||
|
||||
## 已知限制与后续工作
|
||||
|
||||
- **Windows 上读不受限**(ACL runner 只限写);读边界文档在 `@deepseek-ai/dsh-sandbox-windows-acl`。
|
||||
- **Windows workspace-write 的临时区域是真实临时目录**(`GetTempPathW`)。这是有意为之的后端自定义选择,与 Landlock 的决策(`readWrite: ['/tmp', ...]`)同类:seam 的 "backend-defined temp area" 词汇表允许它,`tests/acl.e2e.ts` 的逃逸探针也正是因此位于 temp 树之外。按运行创建私有临时目录(bwrap `--tmpfs /tmp` 的语义)还需 runner 改写环境块——这是可选的进一步加固,而非正确性缺口。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox-windows-acl/README.md
|
||||
README.md: 34a9e7160bae3af93da17cae254202bc1b555967
|
||||
README.zh.md: 0ecfd146a2ef500aa34fe7b6314926a8daf802ab
|
||||
README.md: 7896764867a0266bbd1e26210a2c7cccbcd09d62
|
||||
README.zh.md: 384bc5015167dfa8e4f650c0fcd22e32a62c0ab2
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Windows write-restriction sandbox backend for the [harness sandbox seam](../sandbox/): a Node.js/[koffi](https://koffi.dev/) port of the mechanism in [huoyaoyuan/windows-acl-restrict-poc](https://github.com/huoyaoyuan/windows-acl-restrict-poc) (`10e4dfb`, the fixed revision), built as the preparation layer for a Windows `SandboxProvider` (`workspace-write` / `read-only` modes). Linux/macOS backends live in [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/).
|
||||
Windows write-restriction sandbox backend for the [harness sandbox seam](../sandbox/): a Node.js/[koffi](https://koffi.dev/) port of the mechanism in [huoyaoyuan/windows-acl-restrict-poc](https://github.com/huoyaoyuan/windows-acl-restrict-poc) (`10e4dfb`, the fixed revision), mounted as the win32 rung of the [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/) chain (`workspace-write` / `read-only` modes); the same package carries the Linux/macOS backends.
|
||||
|
||||
Mechanism in one line: the caller's token is duplicated into a `WRITE_RESTRICTED` token whose restricting SIDs include an orphan SID (`S-1-4-x-y`) that only this sandbox instance has added to the workspace and temp directories' DACLs. Windows then grants a write only where BOTH the caller's normal access AND the restricting-SID intersection allow it — the orphan SID is the write allowlist, and it grants nothing anywhere else on the system.
|
||||
|
||||
@@ -11,6 +11,8 @@ Mechanism in one line: the caller's token is duplicated into a `WRITE_RESTRICTED
|
||||
```ts
|
||||
import { AclSandbox } from '@deepseek-ai/dsh-sandbox-windows-acl'
|
||||
|
||||
const workspaceRoot = process.cwd()
|
||||
|
||||
const sandbox = new AclSandbox({ writableDirs: [workspaceRoot] })
|
||||
await sandbox.init() // throws on ANY Win32 failure — never spawns unrestricted
|
||||
|
||||
@@ -56,9 +58,16 @@ The koffi struct definitions assert their sizes against the probe at module load
|
||||
- **Granted directories must be caller-owned.** The owner's implicit `WRITE_DAC` is what lets the sandbox edit the DACL without elevation.
|
||||
- **The temp grant follows `GetTempPathW`** — pass `tempDir` explicitly whenever possible. `GetTempPathW` reads the NATIVE environment block, which host runtimes that manage `process.env` through worker pools may not keep in sync (verified with vitest: a worker-side `process.env.TMP` change never reached the native block). A defaulted grant landing on the real temp dir inherits `(OI)(CI)` over every subdirectory of temp, silently widening the allowlist — point it at a per-sandbox directory instead.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through [`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md), [`dsh-pwsh-sandbox`](../../bash/pwsh-sandbox/README.md), and their tools, which render this backend's enforcement and denial facts (the confined stderr the tool layer classifies through `denialSignatures`) while the [`dsh-sandbox`](../sandbox/README.md) seam owns the `SANDBOX_UNAVAILABLE` text and runner selection.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None directly; the denial surface belongs to the tool layer.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **No `SandboxProvider` wiring yet** — this package is the primitives layer; the `ctx.sandbox.confine()` integration (spawn-side token application plus the `denialSignatures`/`runnerFailureRules` contract) is the next step and cannot reuse the argv-wrapping style of `dsh-sandbox-local` because the restricted token must be applied at `CreateProcess` time.
|
||||
- **One write allowlist per instance** — the orphan SID is the unit of the allowlist; reusing one sandbox instance across two workspaces widens both grants to both roots. Create one instance per workspace root.
|
||||
- **Cleanup is best-effort by design** — `dispose()` attempts every revocation and aggregates failures into an `AggregateError`; a cleanup failure leaves a standing (but orphan-SID-only) ACE that this process's next `init()`/`dispose()` cycle or `icacls` (via the ACE, not the trustee name) can still remove.
|
||||
- **Read-side confinement, network policy, and job-object kill-on-close are out of scope** for this layer and belong to the future provider design.
|
||||
- **Read-side confinement and network policy are out of scope** — `WRITE_RESTRICTED` intersects write accesses only; pair this backend with a read-side policy for stronger confinement.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
面向 [harness 沙盒接口](../sandbox/) 的 Windows 写入限制沙盒后端:用 Node.js/[koffi](https://koffi.dev/) 移植了 [huoyaoyuan/windows-acl-restrict-poc](https://github.com/huoyaoyuan/windows-acl-restrict-poc)(`10e4dfb` 修复版)的机制,作为 Windows 端 `SandboxProvider`(`workspace-write` / `read-only` 模式)实装的准备层。Linux/macOS 后端见 [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/)。
|
||||
面向 [harness 沙盒接口](../sandbox/) 的 Windows 写入限制沙盒后端:用 Node.js/[koffi](https://koffi.dev/) 移植了 [huoyaoyuan/windows-acl-restrict-poc](https://github.com/huoyaoyuan/windows-acl-restrict-poc)(`10e4dfb` 修复版)的机制,作为 [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/) 链的 win32 档(`workspace-write` / `read-only` 模式)挂载;同一包还携带 Linux/macOS 后端。
|
||||
|
||||
一句话机制:把调用者令牌复制为 `WRITE_RESTRICTED` 受限令牌,其 restricting SIDs 中加入一个孤儿 SID(`S-1-4-x-y`),该 SID 只被本沙盒实例加到工作区与临时目录的 DACL 上。此后 Windows 只在「调用者正常权限」与「restricting SID 交集」同时允许时才放行写入——孤儿 SID 就是写入白名单,而它在系统其余位置不授予任何权限。
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
```ts
|
||||
import { AclSandbox } from '@deepseek-ai/dsh-sandbox-windows-acl'
|
||||
|
||||
const workspaceRoot = process.cwd()
|
||||
|
||||
const sandbox = new AclSandbox({ writableDirs: [workspaceRoot] })
|
||||
await sandbox.init() // 任何 Win32 调用失败都会抛错——绝不降级为无沙盒运行
|
||||
await sandbox.init() // throws on ANY Win32 failure — never spawns unrestricted
|
||||
|
||||
const child = sandbox.spawn({ command: 'pwsh', args: ['-NoProfile', '-Command', '...'], cwd: workspaceRoot })
|
||||
const { stdout, stderr, exitCode } = await child.wait()
|
||||
|
||||
sandbox.dispose() // 回收所有挂起的授权;逐项报告清理失败
|
||||
sandbox.dispose() // revokes all standing grants; reports every cleanup failure
|
||||
```
|
||||
|
||||
本包对**每一个** Win32 API 调用都做返回值检查;失败抛出 `Win32Error`,携带 API 名、精确的 Win32 错误码、`FormatMessageW` 系统文本和出错的路径/上下文。这是有意为之:原 POC 忽略所有返回值,当 `CreateRestrictedToken` 失败时会静默地用**完整未受限令牌**运行子进程(fail-open)。本移植从构造上保证 fail-closed。
|
||||
@@ -56,9 +58,16 @@ g++ -std=c++20 -municode -O2 -o abi-probe.exe verify/abi-probe.cpp -ladvapi32 &&
|
||||
- **被授权目录必须归调用者所有。** 所有者隐含的 `WRITE_DAC` 是免提权改 DACL 的前提。
|
||||
- **临时目录授权跟随 `GetTempPathW`** —— 尽可能显式传入 `tempDir`。`GetTempPathW` 读取的是原生环境块,用 worker 池管理 `process.env` 的宿主运行时(vitest 实测)不会把 worker 侧的 `process.env.TMP` 改动同步过去。若默认授权落到真实临时目录,其 `(OI)(CI)` 继承会覆盖 temp 下所有子目录、静默扩大白名单——请指向按沙盒隔离的目录。
|
||||
|
||||
## 模型体验
|
||||
|
||||
经 [`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md)、[`dsh-pwsh-sandbox`](../../bash/pwsh-sandbox/README.md) 及其工具间接生效:它们渲染本后端的强制完整性与拒绝事实(受限 stderr 由工具层按 `denialSignatures` 分类),而 [`dsh-sandbox`](../sandbox/README.md) seam 拥有 `SANDBOX_UNAVAILABLE` 文本与 runner 选择。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无直接影响;拒绝呈现面属于工具层。
|
||||
|
||||
## 已知限制与后续工作
|
||||
|
||||
- **尚未接入 `SandboxProvider`** —— 本包是原语层;`ctx.sandbox.confine()` 的集成(在 spawn 侧应用受限令牌,并补齐 `denialSignatures`/`runnerFailureRules` 契约)是下一步。该集成不能沿用 `dsh-sandbox-local` 的 argv 包装风格,因为受限令牌必须在 `CreateProcess` 时生效。
|
||||
- **每个实例一个写入白名单** —— 孤儿 SID 是白名单的基本单位;同一沙盒实例跨两个工作区复用时,两个根目录会互相扩大授权面。请按工作区根目录各建一个实例。
|
||||
- **清理尽力而为** —— `dispose()` 会尝试全部回收并把失败聚合为 `AggregateError`;清理失败只会留下仅含孤儿 SID 的 ACE,本进程下次 `init()`/`dispose()` 循环或 `icacls`(按 ACE 而非受托者名)仍可清除。
|
||||
- **读侧隔离、网络策略、job-object 关闭即杀** 超出本层范围,留给未来的 provider 设计。
|
||||
- **读侧隔离与网络策略超出范围** —— `WRITE_RESTRICTED` 只对写访问做交集检查;更强的隔离需叠加读侧策略。
|
||||
|
||||
@@ -35,6 +35,9 @@ function buildExplicitAccess(sidPtr: NativePtr, mode: number, permissions: numbe
|
||||
* orphan SID on `path`, inheriting to subcontainers and objects. The directory
|
||||
* must be owned by the caller (owner implicit WRITE_DAC) — same precondition
|
||||
* as the POC.
|
||||
* @param api - the binding table.
|
||||
* @param path - the directory whose DACL gains the grant (the workspace or temp root).
|
||||
* @param sidPtr - the orphan write SID the ACE names.
|
||||
*/
|
||||
export function grantWrite(api: Win32Bindings, path: string, sidPtr: NativePtr): void {
|
||||
const newAclSlot = allocPtrSlot()
|
||||
@@ -63,6 +66,10 @@ export function grantWrite(api: Win32Bindings, path: string, sidPtr: NativePtr):
|
||||
* descriptor allocation — only the descriptor may be LocalFree'd, and it must
|
||||
* not be freed before SetEntriesInAclW has consumed the ACL. Freeing the ACL
|
||||
* pointer itself corrupts the heap (verified the hard way).
|
||||
* @param api - the binding table.
|
||||
* @param path - the directory whose DACL loses the orphan-SID ACEs.
|
||||
* @param sidPtr - the orphan write SID whose ACEs are removed.
|
||||
* @returns whether an ACE removal was attempted (false when the directory carries no DACL at all).
|
||||
*/
|
||||
export function revokeWrite(api: Win32Bindings, path: string, sidPtr: NativePtr): boolean {
|
||||
const ownerSlot = allocPtrSlot()
|
||||
|
||||
@@ -14,9 +14,14 @@ import * as abi from './win32-abi.ts'
|
||||
|
||||
/** Branded koffi 3 native pointer. Koffi 3 pointers are BigInt values; the brand keeps them out of numeric contexts. */
|
||||
declare const nativePtr: unique symbol
|
||||
/** Koffi 3 native pointer (a BigInt address), branded so it cannot silently enter numeric contexts. */
|
||||
export type NativePtr = bigint & { readonly [nativePtr]: true }
|
||||
|
||||
/** True for NULL pointers, however koffi returns them (null or 0n). */
|
||||
/**
|
||||
* True for NULL pointers, however koffi returns them (null or 0n).
|
||||
* @param value - a pointer as koffi may hand it back (pointer, null, or 0n).
|
||||
* @returns a type guard narrowing to the NULL shapes.
|
||||
*/
|
||||
export function isNullPtr(value: NativePtr | null | undefined): value is null | undefined {
|
||||
return value === null || value === undefined || (value as bigint) === 0n
|
||||
}
|
||||
@@ -40,6 +45,7 @@ export interface ProcessInfoOutput {
|
||||
dwThreadId: number
|
||||
}
|
||||
|
||||
/** The lazy koffi binding table: every Win32 call the ACL backend uses, signature-verified against the real headers. */
|
||||
export interface Win32Bindings {
|
||||
// ---- process / token handles --------------------------------------------
|
||||
openProcess(desiredAccess: number, inheritHandle: number, pid: number): NativePtr
|
||||
@@ -111,6 +117,7 @@ export interface Win32Bindings {
|
||||
const PVOID: Ptr = koffi.pointer('void')
|
||||
const PPVOID: Ptr = koffi.pointer(PVOID)
|
||||
|
||||
/** koffi STARTUPINFOW layout; its size is asserted against abi.STARTUPINFOW_SIZE at load. */
|
||||
export const STARTUPINFOW = koffi.struct('STARTUPINFOW', {
|
||||
cb: 'uint32',
|
||||
lpReserved: 'str16',
|
||||
@@ -132,6 +139,7 @@ export const STARTUPINFOW = koffi.struct('STARTUPINFOW', {
|
||||
hStdError: PVOID,
|
||||
})
|
||||
|
||||
/** koffi PROCESS_INFORMATION layout; its size is asserted against abi.PROCESS_INFORMATION_SIZE at load. */
|
||||
export const PROCESS_INFORMATION = koffi.struct('PROCESS_INFORMATION', {
|
||||
hProcess: PVOID,
|
||||
hThread: PVOID,
|
||||
@@ -146,78 +154,127 @@ if (PROCESS_INFORMATION.size !== abi.PROCESS_INFORMATION_SIZE) {
|
||||
throw new Error(`PROCESS_INFORMATION layout mismatch: koffi computed ${PROCESS_INFORMATION.size}, header probe says ${abi.PROCESS_INFORMATION_SIZE}`)
|
||||
}
|
||||
|
||||
/** Allocate one pointer-sized slot (for `T **` out-parameters). */
|
||||
/**
|
||||
* Allocate one pointer-sized slot (for `T **` out-parameters).
|
||||
* @returns the allocated slot pointer.
|
||||
*/
|
||||
export function allocPtrSlot(): NativePtr {
|
||||
const value: unknown = koffi.alloc(PVOID, 1)
|
||||
return value as NativePtr
|
||||
}
|
||||
|
||||
/** Allocate one uint32 slot. */
|
||||
/**
|
||||
* Allocate one uint32 slot.
|
||||
* @returns the allocated slot pointer.
|
||||
*/
|
||||
export function allocUint32(): NativePtr {
|
||||
const value: unknown = koffi.alloc('uint32', 1)
|
||||
return value as NativePtr
|
||||
}
|
||||
|
||||
/** Write a uint32 value into a slot pointer. */
|
||||
/**
|
||||
* Write a uint32 value into a slot pointer.
|
||||
* @param slot - the slot allocated by {@link allocUint32}.
|
||||
* @param value - the uint32 to encode.
|
||||
*/
|
||||
export function encodeUint32(slot: NativePtr, value: number): void {
|
||||
koffi.encode(slot, 'uint32', value)
|
||||
}
|
||||
|
||||
/** Decode the pointer stored in a pointer-sized slot (NULL becomes null). */
|
||||
/**
|
||||
* Decode the pointer stored in a pointer-sized slot (NULL becomes null).
|
||||
* @param slot - the pointer-sized slot holding the out-parameter value.
|
||||
* @returns the decoded pointer, or null for NULL.
|
||||
*/
|
||||
export function decodePtr(slot: NativePtr): NativePtr | null {
|
||||
const value: unknown = koffi.decode(slot, PVOID)
|
||||
if (isNullPtr(value as NativePtr | null | undefined)) return null
|
||||
return value as NativePtr
|
||||
}
|
||||
|
||||
/** Decode a uint32 at a slot pointer. */
|
||||
/**
|
||||
* Decode a uint32 at a slot pointer.
|
||||
* @param slot - the uint32 slot holding the out-parameter value.
|
||||
* @returns the decoded uint32.
|
||||
*/
|
||||
export function decodeUint32(slot: NativePtr): number {
|
||||
const value: unknown = koffi.decode(slot, 'uint32')
|
||||
return value as number
|
||||
}
|
||||
|
||||
/** Decode a UTF-16 string at a pointer. */
|
||||
/**
|
||||
* Decode a UTF-16 string at a pointer.
|
||||
* @param ptr - pointer to the NUL-terminated UTF-16 string.
|
||||
* @returns the decoded string.
|
||||
*/
|
||||
export function decodeStr16(ptr: NativePtr): string {
|
||||
const value: unknown = koffi.decode(ptr, 'str16')
|
||||
return value as string
|
||||
}
|
||||
|
||||
/** Cast a koffi pointer to its numeric address (bigint, used for raw struct packing). */
|
||||
/**
|
||||
* Cast a koffi pointer to its numeric address (bigint, used for raw struct packing).
|
||||
* @param ptr - the koffi pointer.
|
||||
* @returns the pointer's numeric address.
|
||||
*/
|
||||
export function ptrAddress(ptr: NativePtr): bigint {
|
||||
return koffi.address(ptr)
|
||||
}
|
||||
|
||||
/** Allocate a raw byte block (used for SID copies and variable-length arrays). */
|
||||
/**
|
||||
* Allocate a raw byte block (used for SID copies and variable-length arrays).
|
||||
* @param length - the block size in bytes.
|
||||
* @returns the allocated block pointer.
|
||||
*/
|
||||
export function allocBytes(length: number): NativePtr {
|
||||
const value: unknown = koffi.alloc('uint8', length)
|
||||
return value as NativePtr
|
||||
}
|
||||
|
||||
/** Decode a pointer VALUE stored in memory at `buffer[offset]` (e.g. TOKEN_GROUPS entries). */
|
||||
/**
|
||||
* Decode a pointer VALUE stored in memory at `buffer[offset]` (e.g. TOKEN_GROUPS entries).
|
||||
* @param buffer - the buffer holding the pointer value.
|
||||
* @param offset - byte offset of the pointer inside the buffer.
|
||||
* @returns the decoded pointer, or null for NULL.
|
||||
*/
|
||||
export function decodePtrAt(buffer: Buffer, offset: number): NativePtr | null {
|
||||
const value: unknown = koffi.decode(buffer, offset, PVOID)
|
||||
if (isNullPtr(value as NativePtr | null | undefined)) return null
|
||||
return value as NativePtr
|
||||
}
|
||||
|
||||
/** Allocate a zeroed STARTUPINFOW. */
|
||||
/**
|
||||
* Allocate a zeroed STARTUPINFOW.
|
||||
* @returns the allocated struct pointer.
|
||||
*/
|
||||
export function allocStartupInfo(): NativePtr {
|
||||
const value: unknown = koffi.alloc(STARTUPINFOW, 1)
|
||||
return value as NativePtr
|
||||
}
|
||||
|
||||
/** Write the stdio-relevant fields into a zeroed STARTUPINFOW (others stay default-initialized). */
|
||||
/**
|
||||
* Write the stdio-relevant fields into a zeroed STARTUPINFOW (others stay default-initialized).
|
||||
* @param startupInfo - the allocated STARTUPINFOW to encode into.
|
||||
* @param fields - the field subset to write.
|
||||
*/
|
||||
export function encodeStartupInfo(startupInfo: NativePtr, fields: StartupInfoInput): void {
|
||||
koffi.encode(startupInfo, STARTUPINFOW, fields)
|
||||
}
|
||||
|
||||
/** Allocate a zeroed PROCESS_INFORMATION. */
|
||||
/**
|
||||
* Allocate a zeroed PROCESS_INFORMATION.
|
||||
* @returns the allocated struct pointer.
|
||||
*/
|
||||
export function allocProcessInfo(): NativePtr {
|
||||
const value: unknown = koffi.alloc(PROCESS_INFORMATION, 1)
|
||||
return value as NativePtr
|
||||
}
|
||||
|
||||
/** Decode a PROCESS_INFORMATION after CreateProcessAsUserW. */
|
||||
/**
|
||||
* Decode a PROCESS_INFORMATION after CreateProcessAsUserW.
|
||||
* @param processInfo - the PROCESS_INFORMATION filled by the spawn call.
|
||||
* @returns the decoded handle/id fields.
|
||||
*/
|
||||
export function decodeProcessInfo(processInfo: NativePtr): ProcessInfoOutput {
|
||||
const value: unknown = koffi.decode(processInfo, PROCESS_INFORMATION)
|
||||
return value as ProcessInfoOutput
|
||||
@@ -276,12 +333,20 @@ function bindings(): Win32Bindings {
|
||||
return cached
|
||||
}
|
||||
|
||||
/** Resolve the lazy Win32 bindings (throws the first binding failure, fail-closed). */
|
||||
/**
|
||||
* Resolve the lazy Win32 bindings (throws the first binding failure, fail-closed).
|
||||
* @returns the cached binding table.
|
||||
*/
|
||||
export function win32(): Promise<Win32Bindings> {
|
||||
return Promise.resolve(bindings())
|
||||
}
|
||||
|
||||
/** Turn a Win32 error code into readable text via FormatMessageW. */
|
||||
/**
|
||||
* Turn a Win32 error code into readable text via FormatMessageW.
|
||||
* @param api - the binding table.
|
||||
* @param win32Code - the error code to format.
|
||||
* @returns the formatted message text, or '' when formatting fails.
|
||||
*/
|
||||
export function errorText(api: Win32Bindings, win32Code: number): string {
|
||||
const buffer = Buffer.alloc(1024)
|
||||
const length = api.formatMessageW(
|
||||
@@ -295,13 +360,24 @@ export function errorText(api: Win32Bindings, win32Code: number): string {
|
||||
/**
|
||||
* Throw a Win32Error for a BOOL-style API failure. MUST be called immediately
|
||||
* after the failed call so GetLastError is not clobbered by other Win32 calls.
|
||||
* @param api - the binding table.
|
||||
* @param name - the failed API's name for the error message.
|
||||
* @param detail - optional detail overriding the formatted system message.
|
||||
* @returns never — always throws.
|
||||
*/
|
||||
export function throwLastError(api: Win32Bindings, name: string, detail?: string): never {
|
||||
const win32Code = api.getLastError()
|
||||
throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code))
|
||||
}
|
||||
|
||||
/** Throw a Win32Error for an HRESULT-style API return value (the value IS the error code). */
|
||||
/**
|
||||
* Throw a Win32Error for an HRESULT-style API return value (the value IS the error code).
|
||||
* @param api - the binding table.
|
||||
* @param name - the failed API's name for the error message.
|
||||
* @param win32Code - the API's returned error code.
|
||||
* @param detail - optional detail overriding the formatted system message.
|
||||
* @returns never — always throws.
|
||||
*/
|
||||
export function throwWin32(api: Win32Bindings, name: string, win32Code: number, detail?: string): never {
|
||||
throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code))
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import * as abi from './win32-abi.ts'
|
||||
export { quoteArg } from './spawn.ts'
|
||||
export { Win32Error } from './errors.ts'
|
||||
|
||||
/** Construction options: the write allowlist, the optional temp grant, and the orphan SID identity. */
|
||||
export interface AclSandboxOptions {
|
||||
/** Directories the confined child may write into (must exist and be caller-owned). */
|
||||
writableDirs: readonly string[]
|
||||
@@ -51,6 +52,7 @@ export interface AclSandboxOptions {
|
||||
writeSid?: string
|
||||
}
|
||||
|
||||
/** Per-spawn options: the program, its argv/cwd, and the stdio shape. */
|
||||
export interface AclSandboxSpawnOptions {
|
||||
/** Program to run (resolved via PATH search when unqualified, like CreateProcess). */
|
||||
command: string
|
||||
@@ -67,12 +69,14 @@ export interface AclSandboxSpawnOptions {
|
||||
stdio?: 'pipe' | 'inherit'
|
||||
}
|
||||
|
||||
/** A settled confined child: captured stdio and the exit code. */
|
||||
export interface AclSandboxChildResult {
|
||||
stdout: Buffer
|
||||
stderr: Buffer
|
||||
exitCode: number
|
||||
}
|
||||
|
||||
/** A running confined child: its pid and a settlement promise. */
|
||||
export interface AclSandboxChild {
|
||||
/** Child process id. */
|
||||
pid: number
|
||||
@@ -98,7 +102,9 @@ function getTempPath(api: Win32Bindings): string {
|
||||
* failure.
|
||||
*/
|
||||
export class AclSandbox {
|
||||
/** Absolute writable directories (constructor-validated). */
|
||||
readonly writableDirs: string[]
|
||||
/** The orphan SID string whose ACEs form the write allowlist. */
|
||||
readonly writeSid: string
|
||||
private readonly tempDirOption: string | null | undefined
|
||||
private tempDirResolved: string | null | undefined
|
||||
@@ -199,6 +205,8 @@ export class AclSandbox {
|
||||
* placed in a kill-on-close job (dies with the caller). Call dispose() only
|
||||
* after all children have exited — revoking grants under a live child
|
||||
* removes its remaining write allowance.
|
||||
* @param options - the program, argv/cwd, and stdio shape.
|
||||
* @returns the running child.
|
||||
*/
|
||||
spawn(options: AclSandboxSpawnOptions): AclSandboxChild {
|
||||
const api = this.api
|
||||
|
||||
@@ -17,6 +17,8 @@ import * as abi from './win32-abi.ts'
|
||||
* Quote one argument per the CommandLineToArgvW parsing rules (backslash
|
||||
* escaping only before quotes; a trailing backslash before the closing quote
|
||||
* is doubled).
|
||||
* @param argument - one argv entry to quote.
|
||||
* @returns the quoted entry (bare when quoting is unnecessary).
|
||||
*/
|
||||
export function quoteArg(argument: string): string {
|
||||
if (argument === '') return '""'
|
||||
@@ -37,7 +39,12 @@ export function quoteArg(argument: string): string {
|
||||
return quoted + '"'
|
||||
}
|
||||
|
||||
/** Build the single command line CreateProcess parses from program + argv. */
|
||||
/**
|
||||
* Build the single command line CreateProcess parses from program + argv.
|
||||
* @param program - the executable (argv[0]).
|
||||
* @param args - the remaining argv entries.
|
||||
* @returns the joined, quoted command line.
|
||||
*/
|
||||
export function buildCommandLine(program: string, args: readonly string[]): string {
|
||||
return [program, ...args].map(quoteArg).join(' ')
|
||||
}
|
||||
@@ -63,6 +70,7 @@ function setInheritable(api: Win32Bindings, handle: NativePtr, label: string): v
|
||||
}
|
||||
}
|
||||
|
||||
/** A confined child spawned with piped stdio: process handle plus the pipe read ends to drain. */
|
||||
export interface SpawnedNative {
|
||||
pid: number
|
||||
process: NativePtr
|
||||
@@ -74,6 +82,10 @@ export interface SpawnedNative {
|
||||
* Create a process under the restricted token with piped stdio. The child's
|
||||
* stdin is closed immediately (EOF), matching the POC; stdout/stderr read ends
|
||||
* are returned for draining.
|
||||
* @param api - the binding table.
|
||||
* @param token - the restricted token the child runs under.
|
||||
* @param options - command, args, and working directory.
|
||||
* @returns the spawned child's handles.
|
||||
*/
|
||||
export function spawnSandboxed(
|
||||
api: Win32Bindings,
|
||||
@@ -133,7 +145,12 @@ export function spawnSandboxed(
|
||||
}
|
||||
}
|
||||
|
||||
/** Drain one pipe read end to a Buffer via non-blocking PeekNamedPipe polling. */
|
||||
/**
|
||||
* Drain one pipe read end to a Buffer via non-blocking PeekNamedPipe polling.
|
||||
* @param api - the binding table.
|
||||
* @param handle - the pipe read end to drain (closed when done).
|
||||
* @returns the complete pipe contents.
|
||||
*/
|
||||
export async function drainPipe(api: Win32Bindings, handle: NativePtr): Promise<Buffer> {
|
||||
const chunks: Buffer[] = []
|
||||
for (;;) {
|
||||
@@ -167,6 +184,9 @@ export async function drainPipe(api: Win32Bindings, handle: NativePtr): Promise<
|
||||
* the child has already exited, so this wait returns immediately. Calling it
|
||||
* earlier would block the event loop and starve the drains (the pipe-buffer
|
||||
* deadlock the POC comments warn about).
|
||||
* @param api - the binding table.
|
||||
* @param process - the child process handle (closed when done).
|
||||
* @returns the child's exit code.
|
||||
*/
|
||||
export function waitForExit(api: Win32Bindings, process: NativePtr): number {
|
||||
const waitResult = api.waitForSingleObject(process, abi.INFINITE)
|
||||
@@ -197,6 +217,7 @@ function createKillOnCloseJob(api: Win32Bindings): NativePtr {
|
||||
return job
|
||||
}
|
||||
|
||||
/** A confined child spawned with inherited stdio: process handle plus its kill-on-close job. */
|
||||
export interface SpawnedInherited {
|
||||
pid: number
|
||||
process: NativePtr
|
||||
@@ -217,6 +238,10 @@ export interface SpawnedInherited {
|
||||
* STARTF_USESTDHANDLES — otherwise the child receives INVALID std handles
|
||||
* ("The handle is invalid", verified the hard way). The child starts
|
||||
* suspended so it can be assigned to a kill-on-close job before it runs.
|
||||
* @param api - the binding table.
|
||||
* @param token - the restricted token the child runs under.
|
||||
* @param options - command, args, and working directory.
|
||||
* @returns the spawned child's handles and job.
|
||||
*/
|
||||
export function spawnSandboxedInherited(
|
||||
api: Win32Bindings,
|
||||
|
||||
@@ -16,6 +16,8 @@ import * as abi from './win32-abi.ts'
|
||||
* CreateRestrictedToken requires (the POC's OpenProcessToken call; the token
|
||||
* handle is obtained through a real OpenProcess handle because the
|
||||
* GetCurrentProcess() pseudo-handle is not addressable through koffi).
|
||||
* @param api - the binding table.
|
||||
* @returns the opened token handle.
|
||||
*/
|
||||
export function openCurrentProcessToken(api: Win32Bindings): NativePtr {
|
||||
const processHandle = api.openProcess(abi.PROCESS_QUERY_INFORMATION, 0, process.pid)
|
||||
@@ -42,6 +44,9 @@ export function openCurrentProcessToken(api: Win32Bindings): NativePtr {
|
||||
* Find and copy the token's logon session SID (S-1-5-5-x-y, attribute
|
||||
* SE_GROUP_LOGON_ID). The restricted token needs it for WinSta0/desktop and
|
||||
* other per-logon objects; the POC extracts it the same way.
|
||||
* @param api - the binding table.
|
||||
* @param token - the token whose groups are scanned.
|
||||
* @returns a copied logon SID (thrown when the token carries none).
|
||||
*/
|
||||
export function findLogonSid(api: Win32Bindings, token: NativePtr): NativePtr {
|
||||
const neededSlot = allocUint32()
|
||||
@@ -70,7 +75,12 @@ export function findLogonSid(api: Win32Bindings, token: NativePtr): NativePtr {
|
||||
throw new Error(`CreateRestrictedToken prerequisite failed: no logon SID found among ${groupCount} token groups`)
|
||||
}
|
||||
|
||||
/** Create one well-known SID (68-byte buffer) and assert its validity. */
|
||||
/**
|
||||
* Create one well-known SID (68-byte buffer) and assert its validity.
|
||||
* @param api - the binding table.
|
||||
* @param type - the WELL_KNOWN_SID_TYPE to create.
|
||||
* @returns the created SID pointer.
|
||||
*/
|
||||
export function makeWellKnownSid(api: Win32Bindings, type: number): NativePtr {
|
||||
const sid = allocBytes(abi.SECURITY_MAX_SID_SIZE)
|
||||
const sizeSlot = allocUint32()
|
||||
@@ -91,6 +101,7 @@ function buildRestrictingSids(sids: readonly NativePtr[]): Buffer {
|
||||
return buffer
|
||||
}
|
||||
|
||||
/** The well-known SIDs packed into every restricted token's restricting list. */
|
||||
export interface RestrictingSidSet {
|
||||
world: NativePtr
|
||||
authUser: NativePtr
|
||||
@@ -105,6 +116,12 @@ export interface RestrictingSidSet {
|
||||
* write SID that forms the write allowlist. S-1-2-1 (console logon) is
|
||||
* intentionally absent: see win32-abi.ts for the verified failure modes.
|
||||
* FAILS CLOSED: any failure throws — never spawn unrestricted.
|
||||
* @param api - the binding table.
|
||||
* @param currentToken - the process token to restrict.
|
||||
* @param logonSid - the copied logon session SID.
|
||||
* @param writeSid - the orphan SID forming the write allowlist.
|
||||
* @param known - the well-known SIDs entering the restricting list.
|
||||
* @returns the restricted token handle.
|
||||
*/
|
||||
export function createRestrictedToken(
|
||||
api: Win32Bindings,
|
||||
|
||||
@@ -26,101 +26,161 @@
|
||||
// ---- winnt.h ---------------------------------------------------------------
|
||||
|
||||
// TOKEN_* access rights (winnt.h lines ~3928)
|
||||
/** TOKEN_ASSIGN_PRIMARY: required to create a process with the token (CreateProcessAsUser). */
|
||||
export const TOKEN_ASSIGN_PRIMARY = 0x0001
|
||||
/** TOKEN_DUPLICATE: required to duplicate a token (DuplicateTokenEx). */
|
||||
export const TOKEN_DUPLICATE = 0x0002
|
||||
/** TOKEN_QUERY: required to read token information (GetTokenInformation). */
|
||||
export const TOKEN_QUERY = 0x0008
|
||||
/** TOKEN_ADJUST_DEFAULT: required to change a token's default DACL. */
|
||||
export const TOKEN_ADJUST_DEFAULT = 0x0080
|
||||
|
||||
// SID_AND_ATTRIBUTES.Attributes flags (winnt.h lines ~3446)
|
||||
/**
|
||||
* SE_GROUP_LOGON_ID: marks a token group SID as the logon SID (compared with
|
||||
* `>>> 0` — the flag's high bit makes it negative as a signed 32-bit number).
|
||||
*/
|
||||
export const SE_GROUP_LOGON_ID = 0xC0000000
|
||||
|
||||
// Generic file access (winnt.h lines ~5893-5913):
|
||||
// FILE_GENERIC_WRITE = STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES
|
||||
// | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE
|
||||
/** STANDARD_RIGHTS_WRITE (== READ_CONTROL): the standard-rights component of generic write access. */
|
||||
export const STANDARD_RIGHTS_WRITE = 0x00020000 // == READ_CONTROL
|
||||
/** FILE_GENERIC_WRITE: every file-write permission bit plus SYNCHRONIZE. */
|
||||
export const FILE_GENERIC_WRITE = 0x00120116
|
||||
// What the POC grants: FILE_GENERIC_WRITE minus READ_CONTROL; displays as
|
||||
// "Write" in Explorer/icacls (windows-acl-restrict-poc.cpp line 16).
|
||||
/**
|
||||
* GRANT_MASK: FILE_GENERIC_WRITE minus READ_CONTROL — the write-access mask
|
||||
* the orphan-SID ACEs grant (displays as "Write" in Explorer/icacls).
|
||||
*/
|
||||
export const GRANT_MASK = FILE_GENERIC_WRITE & ~STANDARD_RIGHTS_WRITE // 0x00100116
|
||||
|
||||
// CreateRestrictedToken flags (winnt.h lines ~4284)
|
||||
/** DISABLE_MAX_PRIVILEGE: strip the token's maximum-privilege elevation so the confined child cannot escalate. */
|
||||
export const DISABLE_MAX_PRIVILEGE = 0x1
|
||||
/** LUA_TOKEN: produce a limited-user (filtered admin) token. */
|
||||
export const LUA_TOKEN = 0x4
|
||||
/** WRITE_RESTRICTED: intersect write access with the restricting SIDs' ACL grants — the sandbox's core mechanism. */
|
||||
export const WRITE_RESTRICTED = 0x8
|
||||
|
||||
// WELL_KNOWN_SID_TYPE (winnt.h lines ~3369-3407)
|
||||
/** WinWorldSid: S-1-1-0 (Everyone). */
|
||||
export const WinWorldSid = 1
|
||||
/** WinLocalSid: S-1-2-0 (LOCAL); CreateWellKnownSid(WinLocalSid) fails with ERROR_INVALID_PARAMETER on Windows 11 build 26200. */
|
||||
export const WinLocalSid = 2
|
||||
/** WinInteractiveSid: S-1-5-4 (INTERACTIVE). */
|
||||
export const WinInteractiveSid = 11
|
||||
/** WinAuthenticatedUserSid: S-1-5-11 (Authenticated Users). */
|
||||
export const WinAuthenticatedUserSid = 17
|
||||
|
||||
// TOKEN_INFORMATION_CLASS (winnt.h line ~3963: TokenUser=1, TokenGroups=2)
|
||||
/** TokenGroups: GetTokenInformation class returning the token's group SIDs. */
|
||||
export const TokenGroups = 2
|
||||
|
||||
// SECURITY_INFORMATION (winnt.h line ~4293)
|
||||
/** DACL_SECURITY_INFORMATION: read/write only the DACL of a security descriptor. */
|
||||
export const DACL_SECURITY_INFORMATION = 0x00000004
|
||||
|
||||
// PROCESS access rights (winnt.h lines ~4364)
|
||||
/** PROCESS_QUERY_INFORMATION: read exit status and times of a process handle. */
|
||||
export const PROCESS_QUERY_INFORMATION = 0x0400
|
||||
|
||||
// ---- accctrl.h -------------------------------------------------------------
|
||||
|
||||
// SE_OBJECT_TYPE (accctrl.h line ~22: SE_UNKNOWN_OBJECT_TYPE=0, SE_FILE_OBJECT=1)
|
||||
/** SE_FILE_OBJECT: the trustee path names a filesystem object. */
|
||||
export const SE_FILE_OBJECT = 1
|
||||
|
||||
// TRUSTEE_FORM / TRUSTEE_TYPE (accctrl.h lines ~38-55): both enums start at 0
|
||||
/** TRUSTEE_IS_UNKNOWN: TRUSTEE_TYPE unknown (TrusteeForm carries the shape). */
|
||||
export const TRUSTEE_IS_UNKNOWN = 0
|
||||
/** TRUSTEE_IS_SID: TRUSTEE_FORM — Trustee.ptstrName is a SID pointer. */
|
||||
export const TRUSTEE_IS_SID = 0
|
||||
/** NO_MULTIPLE_TRUSTEE: Trustee.pMultipleTrustee is null. */
|
||||
export const NO_MULTIPLE_TRUSTEE = 0
|
||||
|
||||
// ACCESS_MODE (accctrl.h line ~127: NOT_USED_ACCESS=0, GRANT_ACCESS=1, REVOKE_ACCESS=4)
|
||||
/** GRANT_ACCESS: SetEntriesInAclW adds the entry as an allow ACE. */
|
||||
export const GRANT_ACCESS = 1
|
||||
/** REVOKE_ACCESS: SetEntriesInAclW removes the matching allow ACE. */
|
||||
export const REVOKE_ACCESS = 4
|
||||
|
||||
// grfInheritance (accctrl.h lines ~137-142)
|
||||
/**
|
||||
* SUB_CONTAINERS_AND_OBJECTS_INHERIT: the ACE applies to the directory, its
|
||||
* subdirectories, and files (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE).
|
||||
*/
|
||||
export const SUB_CONTAINERS_AND_OBJECTS_INHERIT = 0x3 // == OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE
|
||||
|
||||
// ---- winbase.h -------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* STARTF_USESTDHANDLES: STARTUPINFOW dwFlags — the child uses the hStd*
|
||||
* handles, required because Node clears stdio inheritability at startup.
|
||||
*/
|
||||
export const STARTF_USESTDHANDLES = 0x00000100
|
||||
/** HANDLE_FLAG_INHERIT: SetHandleInformation flag re-enabling handle inheritance for the spawned child's stdio handles. */
|
||||
export const HANDLE_FLAG_INHERIT = 0x1
|
||||
/** INFINITE: never-timeout wait value. */
|
||||
export const INFINITE = 0xFFFFFFFF
|
||||
/** MAX_PATH: legacy path length bound. */
|
||||
export const MAX_PATH = 260
|
||||
// winbase.h line ~410: the confined child starts suspended so the runner can
|
||||
// assign it to the kill-on-close job before any of its code runs.
|
||||
/** CREATE_SUSPENDED: create the child with its primary thread suspended until ResumeThread. */
|
||||
export const CREATE_SUSPENDED = 0x4
|
||||
// winbase.h lines ~497-499: GetStdHandle selectors.
|
||||
/** STD_INPUT_HANDLE: GetStdHandle selector for the standard input. */
|
||||
export const STD_INPUT_HANDLE = -10
|
||||
/** STD_OUTPUT_HANDLE: GetStdHandle selector for the standard output. */
|
||||
export const STD_OUTPUT_HANDLE = -11
|
||||
/** STD_ERROR_HANDLE: GetStdHandle selector for the standard error. */
|
||||
export const STD_ERROR_HANDLE = -12
|
||||
|
||||
// FormatMessageW flags (winbase.h lines ~1446-1469)
|
||||
/** FORMAT_MESSAGE_FROM_SYSTEM: format the message from the system message table. */
|
||||
export const FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
|
||||
/** FORMAT_MESSAGE_IGNORE_INSERTS: skip insert-sequence substitution. */
|
||||
export const FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
|
||||
|
||||
// ---- error codes -----------------------------------------------------------
|
||||
|
||||
/** ERROR_SUCCESS: the operation succeeded. */
|
||||
export const ERROR_SUCCESS = 0
|
||||
/** ERROR_INSUFFICIENT_BUFFER: a size-probe call succeeded but needs a larger buffer. */
|
||||
export const ERROR_INSUFFICIENT_BUFFER = 122
|
||||
/** ERROR_BROKEN_PIPE: the pipe's other end has closed. */
|
||||
export const ERROR_BROKEN_PIPE = 109
|
||||
/** ERROR_NO_DATA: the pipe is being closed. */
|
||||
export const ERROR_NO_DATA = 232
|
||||
|
||||
// ---- job object (winnt.h lines ~4859-4866, ~5138, ~5190-5199) --------------
|
||||
|
||||
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: the child dies when the runner's last
|
||||
// job handle closes — the orphan-child backstop for the runner design.
|
||||
/** JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: the child dies when the runner's last job handle closes — the orphan-child backstop. */
|
||||
export const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
||||
// JOBOBJECTINFOCLASS: JobObjectBasicAccountingInformation=1, ..., ExtendedLimit=9.
|
||||
/** JobObjectExtendedLimitInformation: JOBOBJECTINFOCLASS for the extended limit structure. */
|
||||
export const JobObjectExtendedLimitInformation = 9
|
||||
// sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION), verified by abi-probe.
|
||||
/** sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION), verified by abi-probe. */
|
||||
export const JOBOBJECT_EXTENDED_LIMIT_SIZE = 144
|
||||
// LimitFlags offset inside JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
// (BasicLimitInformation@0 + PerProcessUserTimeLimit@0 + PerJobUserTimeLimit@8),
|
||||
// verified by abi-probe.
|
||||
/**
|
||||
* LimitFlags offset inside JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
* (BasicLimitInformation@0 + PerProcessUserTimeLimit@0 +
|
||||
* PerJobUserTimeLimit@8), verified by abi-probe.
|
||||
*/
|
||||
export const JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET = 16
|
||||
|
||||
// ---- ABI layout, verified by verify/abi-probe.cpp (x64) --------------------
|
||||
|
||||
/** SECURITY_MAX_SID_SIZE: maximum SID byte size. */
|
||||
export const SECURITY_MAX_SID_SIZE = 68
|
||||
/** SID_AND_ATTRIBUTES stride: { PSID Sid @0 (8); DWORD Attributes @8 (4) } + pad. */
|
||||
export const SID_AND_ATTRIBUTES_SIZE = 16
|
||||
@@ -132,5 +192,7 @@ export const EXPLICIT_ACCESS_W_SIZE = 48
|
||||
export const TRUSTEE_W_OFFSET = 16
|
||||
/** ptstrName offset inside TRUSTEE_W (=> 40 inside EXPLICIT_ACCESS_W). */
|
||||
export const TRUSTEE_W_PTSTRNAME_OFFSET = 24
|
||||
/** sizeof(STARTUPINFOW), verified by abi-probe. */
|
||||
export const STARTUPINFOW_SIZE = 104
|
||||
/** sizeof(PROCESS_INFORMATION), verified by abi-probe. */
|
||||
export const PROCESS_INFORMATION_SIZE = 24
|
||||
|
||||
@@ -97,6 +97,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/subprocess/subprocess': { kind: 'indirect', reason: 'The seam delegates all model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/subprocess/subprocess-local': { kind: 'indirect', reason: 'The spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
|
||||
'packages/sandbox/sandbox-windows-acl': { kind: 'indirect', reason: 'The provider backend delegates model rendering to the bash/pwsh sandbox executors and their tools.' },
|
||||
'packages/sdk/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' },
|
||||
'packages/sdk/helper': { kind: 'none', reason: 'The project domain edits files and registers no live agent or model surface.' },
|
||||
'packages/sdk/scripts': { kind: 'indirect', reason: 'The launcher delegates model context to the loaded project plugin tree.' },
|
||||
|
||||
Reference in New Issue
Block a user