@deepseek-ai/dsh-pwsh-sandbox 0.1.6-alpha.2 → 0.1.7-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/shell/pwsh-sandbox/README.md
5
- README.md: cddcce511d01f3c10208304ea55bf6156ccf2429
6
- README.zh.md: 40aa055770e526cc41607f0e4cd63273b80ebc15
5
+ README.md: 8d75347efa49ff2697bab7a3fd5b6195faf6685f
6
+ README.zh.md: 295d60d9a63d4f039e49e52385561e51194bd25e
package/README.md CHANGED
@@ -35,7 +35,7 @@ Choose it when a deployment needs file-level confinement for PowerShell commands
35
35
 
36
36
  | Mode | File effects |
37
37
  |---|---|
38
- | `read-only` (default) | Writes are denied; the boundary stays partial because the restricted token retains Everyone |
38
+ | `read-only` (default) | Writes are denied; the boundary stays partial for the shared hard-link, unconfined-read, and AppContainer-ACL limits |
39
39
  | `workspace-write` | Writes under the policy's workspace root plus a private temp directory; `TMP`/`TEMP` are rewritten to it before spawning |
40
40
  | `danger-full-access` | No confinement; the provider is never consulted, and results carry `sandbox: { mode, denied: false }` |
41
41
 
@@ -75,20 +75,20 @@ This section explains the design of the executor and points at the code that rea
75
75
 
76
76
  ### Design concept
77
77
 
78
- The executor is the pwsh twin of `dsh-bash-sandbox`: it inherits `dsh-pwsh-local`'s process mechanics, consumes its argv-level seam (`argv()`/`runArgv()`/`startArgv()`/`onProcessDone()`), and awaits confinement of the exact pwsh invocation through `ctx.sandbox.confine()` before spawning. Foreground preparation uses the local executor’s shared command deadline; timeout before spawn carries no enforcement claim. Background preparation follows only the caller signal. Both paths recheck cancellation before spawn. The confinement substance is platform-neutral — the sandbox seam resolves to the platform's runner — while this package owns the pwsh side only: the selected mode, enforcement completeness, and denial classification on results.
78
+ The executor is the pwsh twin of `dsh-bash-sandbox`: it inherits `dsh-pwsh-local`'s process mechanics, consumes its argv-level seam (`argv()`/`executeArgv()`/`onProcessDone()`), and wraps the exact pwsh invocation through `ctx.sandbox.confine()` before spawning. The confinement substance is platform-neutral — the sandbox seam resolves to the platform's runner — while this package owns the pwsh side only: the selected mode, enforcement completeness, and denial classification on results.
79
79
 
80
80
  ### Source map
81
81
 
82
82
  | File | Role |
83
83
  |---|---|
84
- | [`src/index.ts`](src/index.ts) | Plugin entry: `SandboxPwshExecutor`, per-process fact retention, run/start wrapping |
84
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `SandboxPwshExecutor`, per-process fact retention, execution preparation |
85
85
  | [`src/helpers.ts`](src/helpers.ts) | Denial, runner-failure, and runner-spawn-failure classification |
86
86
  | — | No runtime invariant companion is published; this package exposes no independent event sequence or mutable data relation beyond contracts enforced at its owning seams. Classification is observable in results. |
87
87
  | `tests/` | Exercised behavior across the ACL and platform runners |
88
88
 
89
89
  ### Main flow
90
90
 
91
- For a confined mode, `resolve()` stamps the per-call policy; `run` and `start` wrap the pwsh argv through the provider and hand the confined argv to the inherited subprocess path. At settlement the executor classifies the outcome: a runner failure outranks a denial because the command never ran, a failed run whose stderr carries the runner's denial dialect is reported `denied: true`, and every confined run carries its mode and enforcement facts. `danger-full-access` bypasses the provider entirely and stamps `denied: false`.
91
+ For a confined mode, `resolve()` stamps the per-call policy; `execute` awaits preparation of the pwsh argv through the provider and hands the confined argv to the inherited subprocess path. At settlement the executor classifies the outcome: a runner failure outranks a denial because the command never ran, a failed run whose stderr carries the runner's denial dialect is reported `denied: true`, and every confined run carries its mode and enforcement facts. `danger-full-access` bypasses the provider entirely and stamps `denied: false`.
92
92
 
93
93
  ### Invariants
94
94
 
@@ -140,7 +140,7 @@ These limits define when this executor is only a partial boundary on Windows. Th
140
140
 
141
141
  - **Reads are unrestricted on Windows** — the ACL runner restricts writes only; the read boundary is documented in `@deepseek-ai/dsh-sandbox-windows-acl`.
142
142
  - **Windows workspace-write temp authority is private** — per live session/workspace pair; agentless calls receive a fresh private directory per invocation; the ambient temp root is never granted, and the runner rewrites `TMP`/`TEMP` to the private directory before spawning.
143
- - **Windows read-only grants no explicit writable root but remains partial** — the restricted token must retain Everyone; objects whose DACL grants Everyone write access — including compatible opens of the NUL device — remain ambient authority, while PowerShell's `> $null` redirection still works without opening NUL.
143
+ - **Windows read-only grants no explicit writable root but remains partial** — NTFS hard links alias one file object across paths, reads stay unconfined, and a tree another AppContainer tool has ACL'd with a package SID is unreadable to the Low-integrity child. NUL stays writable in both modes because the device DACL grants Everyone write and carries no higher label; PowerShell's `> $null` redirection still works without opening it.
144
144
 
145
145
  <a id="dev-note"></a>
146
146
  ### Dev Note
package/README.zh.md CHANGED
@@ -35,7 +35,7 @@ kind: "package-reference"
35
35
 
36
36
  | 模式 | 文件影响 |
37
37
  |---|---|
38
- | `read-only`(默认) | 写入被拒绝;由于受限令牌必须保留 Everyone,边界仍是不完整的 |
38
+ | `read-only`(默认) | 写入被拒绝;边界仍受共享的硬链接、读取不受限与 AppContainer ACL 限制 |
39
39
  | `workspace-write` | 只能写入策略的工作区根目录加一个私有临时目录;spawn 前 `TMP`/`TEMP` 会被重写到该目录 |
40
40
  | `danger-full-access` | 不作限制;绝不咨询提供方,结果携带 `sandbox: { mode, denied: false }` |
41
41
 
@@ -75,20 +75,20 @@ kind: "package-reference"
75
75
 
76
76
  ### 设计概念
77
77
 
78
- 本执行器是 `dsh-bash-sandbox` 的 pwsh 孪生:它继承 `dsh-pwsh-local` 的进程机制,消费其 argv 级 seam(`argv()`/`runArgv()`/`startArgv()`/`onProcessDone()`),并在 spawn 前通过 `ctx.sandbox.confine()` 等待精确的 pwsh 调用完成限制准备。前台准备使用本地执行器与命令共享的 deadline;在 spawn 前超时不会声明 enforcement 事实。后台准备只跟随调用方信号。两条路径都在 spawn 前重新检查取消状态。隔离实体本身是平台无关的——沙箱 seam 解析到平台的 runner——而本包只负责 pwsh 侧:所选模式、强制执行完整度,以及结果上的拒绝分类。
78
+ 本执行器是 `dsh-bash-sandbox` 的 pwsh 孪生:它继承 `dsh-pwsh-local` 的进程机制,消费其 argv 级 seam(`argv()`/`executeArgv()`/`onProcessDone()`),并在 spawn 前把精确的 pwsh 调用经 `ctx.sandbox.confine()` 包装。隔离实体本身是平台无关的——沙箱 seam 解析到平台的 runner——而本包只负责 pwsh 侧:所选模式、强制执行完整度,以及结果上的拒绝分类。
79
79
 
80
80
  ### 源码地图
81
81
 
82
82
  | 文件 | 职责 |
83
83
  |---|---|
84
- | [`src/index.ts`](src/index.ts) | 插件入口:`SandboxPwshExecutor`、按进程保留事实、run/start 包装 |
84
+ | [`src/index.ts`](src/index.ts) | 插件入口:`SandboxPwshExecutor`、按进程保留事实、执行准备 |
85
85
  | [`src/helpers.ts`](src/helpers.ts) | 拒绝、runner 失败与 runner spawn 失败分类 |
86
86
  | — | 不发布运行时不变式伴生入口;除所属 seam 所执行的约定外,本包不暴露独立事件序列或可变数据关系;分类可在结果中观察。 |
87
87
  | `tests/` | 跨 ACL 与平台 runner 演练的行为 |
88
88
 
89
89
  ### 主要流程
90
90
 
91
- 对受限模式,`resolve()` 标记每次调用的策略;`run` 与 `start` 把 pwsh argv 经提供方包装,再把受限 argv 交给继承的子进程路径。结算时执行器对结果分类:runner 失败优先于拒绝(命令从未运行),stderr 携带 runner 拒绝方言的失败运行报告 `denied: true`,每次受限运行都携带模式与强制执行事实。`danger-full-access` 完全绕过提供方,并标记 `denied: false`。
91
+ 对受限模式,`resolve()` 标记每次调用的策略;`execute` 把 pwsh argv 经提供方包装,再把受限 argv 交给继承的子进程路径。结算时执行器对结果分类:runner 失败优先于拒绝(命令从未运行),stderr 携带 runner 拒绝方言的失败运行报告 `denied: true`,每次受限运行都携带模式与强制执行事实。`danger-full-access` 完全绕过提供方,并标记 `denied: false`。
92
92
 
93
93
  ### 不变式
94
94
 
@@ -140,7 +140,7 @@ kind: "package-reference"
140
140
 
141
141
  - **Windows 上读不受限**——ACL runner 只限写;读边界文档在 `@deepseek-ai/dsh-sandbox-windows-acl`。
142
142
  - **Windows workspace-write 的临时权限按每个活跃的会话/工作区对私有**——无 agent(智能体)的调用每次都获得一个新的私有目录;环境临时根目录绝不会被授权,runner 会在 spawn 前将 `TMP`/`TEMP` 重写为该私有目录。
143
- - **Windows read-only 不授予任何显式可写根目录,但仍为部分强制执行**——受限令牌必须保留 Everyone;DACL 向 Everyone 授予写访问的对象——包括以兼容方式打开的 NUL 设备——仍构成环境权限来源,而 PowerShell 的 `> $null` 重定向仍可工作,且不会打开 NUL。
143
+ - **Windows read-only 不授予任何显式可写根目录,但仍为部分强制执行**——NTFS 硬链接会把同一文件对象别名为多个路径、读取仍不受限,且被其他 AppContainer 工具以包 SID 标记过的目录树对 Low 完整性子进程不可读。NUL 在两种模式下仍可写,因为设备 DACL 向 Everyone 授予写权限且不带更高的标签;PowerShell 的 `> $null` 重定向仍然工作,且不会打开 NUL。
144
144
 
145
145
  <a id="dev-note"></a>
146
146
  ### 开发备注
package/lib/index.js CHANGED
@@ -115,7 +115,7 @@ function matchesSignature(exitCode, stderr, signatures) {
115
115
  * `result.sandbox` reports the mode, enforcement, and denial facts the tool
116
116
  * renders.
117
117
  */
118
- var SandboxPwshExecutor = class extends PwshLocalExecutor {
118
+ var SandboxPwshExecutor = class SandboxPwshExecutor extends PwshLocalExecutor {
119
119
  static inject = [
120
120
  "subprocess",
121
121
  "sandbox",
@@ -148,79 +148,74 @@ var SandboxPwshExecutor = class extends PwshLocalExecutor {
148
148
  sandboxPolicy: request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
149
149
  };
150
150
  }
151
- async run(spec) {
151
+ async execute(spec) {
152
152
  const policy = spec.sandboxPolicy;
153
153
  const { mode } = policy;
154
- if (mode === "danger-full-access") return {
155
- ...await super.run(spec),
154
+ if (mode === "danger-full-access") return SandboxPwshExecutor.decorateResult(await super.execute(spec), (result) => ({
155
+ ...result,
156
156
  sandbox: {
157
157
  mode,
158
158
  denied: false
159
159
  }
160
- };
160
+ }));
161
161
  let confined;
162
- let result;
163
- let spawnRequested;
164
- try {
165
- ({result, spawnRequested} = await this.runArgv(spec, async (signal) => {
166
- const prepared = await this.confine(spec, {
167
- ...policy,
168
- mode
169
- }, signal);
170
- signal.throwIfAborted();
171
- confined = prepared;
172
- return prepared.argv;
173
- }));
174
- } catch (error) {
162
+ const ex = await this.executeArgv(spec, async (signal) => {
163
+ const prepared = await this.confine(spec, {
164
+ ...policy,
165
+ mode
166
+ }, signal);
167
+ signal.throwIfAborted();
168
+ confined = prepared;
169
+ return prepared.argv;
170
+ }, (process) => {
171
+ const facts = confined;
172
+ this.processFacts.set(process, {
173
+ mode,
174
+ enforcement: facts.enforcement,
175
+ denialSignatures: facts.denialSignatures,
176
+ runnerFailureRules: facts.runnerFailureRules,
177
+ runnerProgram: facts.argv[0],
178
+ workdir: spec.workdir
179
+ });
180
+ });
181
+ return SandboxPwshExecutor.decorateResult(ex, (result) => {
182
+ if (confined === void 0) return {
183
+ ...result,
184
+ sandbox: {
185
+ mode,
186
+ denied: false
187
+ }
188
+ };
189
+ const { enforcement, denialSignatures, runnerFailureRules } = confined;
190
+ const runnerFailure = classifyRunnerFailure(result.exitCode, result.stderr.text, runnerFailureRules);
191
+ if (runnerFailure !== void 0) throw new SandboxUnavailableError(mode, runnerFailure.detail);
192
+ return {
193
+ ...result,
194
+ sandbox: {
195
+ mode,
196
+ denied: classifyDenial(result, denialSignatures),
197
+ enforcement
198
+ }
199
+ };
200
+ }, (error) => {
175
201
  if (spec.signal?.aborted === true) spec.signal.throwIfAborted();
176
202
  if (confined !== void 0 && isRunnerSpawnFailure(error, confined.argv[0], spec.workdir)) throw new SandboxUnavailableError(mode, String(error));
177
203
  throw error;
178
- }
179
- if (!spawnRequested) return {
180
- ...result,
181
- sandbox: {
182
- mode,
183
- denied: false
184
- }
185
- };
186
- const facts = confined;
187
- const runnerFailure = classifyRunnerFailure(result.exitCode, result.stderr.text, facts.runnerFailureRules);
188
- if (runnerFailure !== void 0) throw new SandboxUnavailableError(mode, runnerFailure.detail);
189
- return {
190
- ...result,
191
- sandbox: {
192
- mode,
193
- denied: classifyDenial(result, facts.denialSignatures),
194
- enforcement: facts.enforcement
195
- }
196
- };
197
- }
198
- async start(spec) {
199
- const policy = spec.sandboxPolicy;
200
- const { mode } = policy;
201
- if (mode === "danger-full-access") return super.start(spec);
202
- const confined = await this.confine(spec, {
203
- ...policy,
204
- mode
205
- }, spec.signal);
206
- spec.signal?.throwIfAborted();
207
- let proc;
208
- try {
209
- proc = this.startArgv(spec, confined.argv);
210
- } catch (error) {
211
- if (isRunnerSpawnFailure(error, confined.argv[0], spec.workdir)) throw new SandboxUnavailableError(mode, String(error));
212
- throw error;
213
- }
214
- const { enforcement, denialSignatures, runnerFailureRules } = confined;
215
- this.processFacts.set(proc, {
216
- mode,
217
- enforcement,
218
- denialSignatures,
219
- runnerFailureRules,
220
- runnerProgram: confined.argv[0],
221
- workdir: spec.workdir
222
204
  });
223
- return proc;
205
+ }
206
+ /**
207
+ * Decorate the handle's foreground projection in place, memoized once. The
208
+ * handle keeps its identity (never wrapped in a second object) because the
209
+ * per-process facts and `onProcessDone` key on the exact instance.
210
+ */
211
+ static decorateResult(ex, map, mapError) {
212
+ const base = ex.result.bind(ex);
213
+ let decorated;
214
+ ex.result = () => {
215
+ decorated ??= base().then(map, mapError);
216
+ return decorated;
217
+ };
218
+ return ex;
224
219
  }
225
220
  /**
226
221
  * Stamp per-process sandbox facts before `done` settles. Full-access
@@ -12,7 +12,7 @@
12
12
  * @module @deepseek-ai/dsh-pwsh-sandbox
13
13
  */
14
14
  import { Context } from '@deepseek-ai/cordis';
15
- import type { ShellExecRequest, ShellExecSpec, ShellProcess, ShellRunResult } from '@deepseek-ai/dsh-shell';
15
+ import type { ShellExecRequest, ShellExecSpec, ShellExecution, ShellProcess } from '@deepseek-ai/dsh-shell';
16
16
  import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
17
17
  import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local';
18
18
  import type { Config as LocalConfig } from '@deepseek-ai/dsh-pwsh-local';
@@ -53,8 +53,13 @@ export declare class SandboxPwshExecutor extends PwshLocalExecutor {
53
53
  * the deployment policy.
54
54
  */
55
55
  resolve(request: ShellExecRequest): ShellExecSpec;
56
- run(spec: ShellExecSpec): Promise<ShellRunResult>;
57
- start(spec: ShellExecSpec): Promise<ShellProcess>;
56
+ execute(spec: ShellExecSpec): Promise<ShellExecution>;
57
+ /**
58
+ * Decorate the handle's foreground projection in place, memoized once. The
59
+ * handle keeps its identity (never wrapped in a second object) because the
60
+ * per-process facts and `onProcessDone` key on the exact instance.
61
+ */
62
+ private static decorateResult;
58
63
  /**
59
64
  * Stamp per-process sandbox facts before `done` settles. Full-access
60
65
  * processes have no facts; signal deaths are not denials.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-pwsh-sandbox",
3
3
  "description": "Sandbox-consuming implementation of the DeepSeek Harness PowerShell executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)",
4
- "version": "0.1.6-alpha.2",
4
+ "version": "0.1.7-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,20 +27,20 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "peerDependencies": {
30
- "@deepseek-ai/dsh-shell": "^0.1.6-alpha.2",
31
- "@deepseek-ai/dsh-pwsh-local": "^0.1.6-alpha.2",
32
- "@deepseek-ai/dsh-sandbox": "^0.1.6-alpha.2",
33
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.6-alpha.2",
34
- "@deepseek-ai/cordis": "^4.0.2"
30
+ "@deepseek-ai/dsh-pwsh-local": "0.1.7-alpha.2",
31
+ "@deepseek-ai/dsh-shell": "0.1.7-alpha.2",
32
+ "@deepseek-ai/dsh-sandbox": "0.1.7-alpha.2",
33
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.7-alpha.2",
34
+ "@deepseek-ai/cordis": "~4.0.4"
35
35
  },
36
36
  "devDependencies": {
37
- "@deepseek-ai/dsh-shell": "^0.1.6-alpha.2",
38
- "@deepseek-ai/dsh-pwsh-local": "^0.1.6-alpha.2",
39
- "@deepseek-ai/dsh-sandbox": "^0.1.6-alpha.2",
40
- "@deepseek-ai/dsh-sandbox-local": "^0.1.6-alpha.2",
41
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.6-alpha.2",
42
- "@deepseek-ai/dsh-session-projection": "^0.1.6-alpha.2",
43
- "@deepseek-ai/dsh-subprocess-local": "^0.1.6-alpha.2",
44
- "@deepseek-ai/cordis": "^4.0.2"
37
+ "@deepseek-ai/dsh-shell": "0.1.7-alpha.2",
38
+ "@deepseek-ai/dsh-pwsh-local": "0.1.7-alpha.2",
39
+ "@deepseek-ai/dsh-sandbox": "0.1.7-alpha.2",
40
+ "@deepseek-ai/dsh-sandbox-local": "0.1.7-alpha.2",
41
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.7-alpha.2",
42
+ "@deepseek-ai/dsh-session-projection": "0.1.7-alpha.2",
43
+ "@deepseek-ai/dsh-subprocess-local": "0.1.7-alpha.2",
44
+ "@deepseek-ai/cordis": "~4.0.4"
45
45
  }
46
46
  }