@deepseek-ai/dsh-pwsh-sandbox 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.
- package/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +34 -0
- package/README.zh.md +34 -0
- package/lib/index.js +241 -0
- package/lib/invariant.js +23 -0
- package/lib/types/helpers.d.ts +57 -0
- package/lib/types/index.d.ts +74 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/bash/pwsh-sandbox/README.md
|
|
5
|
+
README.md: bd506d011fa6167ddf7d6fe0565e475979ad0ec2
|
|
6
|
+
README.zh.md: e9aa380302037be3c9dd07331035544299bf3bec
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-pwsh-sandbox
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Sandbox-consuming PowerShell implementation of the [`ctx.bash` executor seam](../bash/): every command runs as `pwsh -NoLogo -NoProfile -NonInteractive -Command <command>` **confined through `ctx.sandbox`**, with the selected mode, enforcement, and denial facts stamped on each settled result. The pwsh twin of [`@deepseek-ai/dsh-bash-sandbox`](../bash-sandbox/), a call-for-call mirror per the [pwsh executor and tool decision](../../../.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md) — the confinement substance is platform-neutral: on Windows the sandbox seam resolves to the ACL restricted-token runner chain ([`@deepseek-ai/dsh-sandbox-windows-acl`](../../sandbox/sandbox-windows-acl/)), on Linux/macOS to bwrap/Landlock/Seatbelt.
|
|
6
|
+
|
|
7
|
+
The executor inherits [`@deepseek-ai/dsh-pwsh-local`](../pwsh-local/)'s process mechanics and consumes its argv-level seam (`argv()` / `runArgv()` / `startArgv()` / `onProcessDone()`) to wrap the exact pwsh invocation through the provider. The sandbox policy (mode + workspace root) is NOT this package's config: it rides each call from `ctx.sandboxPolicy` (tool calls pass the calling session's resolved policy; direct calls fall back to deployment policy).
|
|
8
|
+
|
|
9
|
+
## Behavior
|
|
10
|
+
|
|
11
|
+
- `danger-full-access`: commands run through the local executor unchanged; results carry `sandbox: { mode, denied: false }`.
|
|
12
|
+
- Confined modes (`read-only`, `workspace-write`): the pwsh argv is wrapped by `ctx.sandbox.confine()`; runner-launch refusal fails closed with `SANDBOX_UNAVAILABLE` (foreground throw, background `runnerFailed` fact), and a denied write classifies against the selected backend's `denialSignatures` into `sandbox.denied`.
|
|
13
|
+
|
|
14
|
+
## Model Experience
|
|
15
|
+
|
|
16
|
+
### Confinement works, denial surfaces as command failure
|
|
17
|
+
|
|
18
|
+
#### What the model sees
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
#### Token effect
|
|
23
|
+
|
|
24
|
+
No model-visible text beyond the command's stderr and the tool layer's standard denial surface.
|
|
25
|
+
|
|
26
|
+
#### KV Cache effect
|
|
27
|
+
|
|
28
|
+
None directly; the denial surface belongs to the tool layer.
|
|
29
|
+
|
|
30
|
+
## Known Limitations and Deferred Work
|
|
31
|
+
|
|
32
|
+
- **Reads are unrestricted** on Windows (the ACL runner restricts writes only); the read boundary is documented in `@deepseek-ai/dsh-sandbox-windows-acl`.
|
|
33
|
+
- **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.
|
|
34
|
+
- **Windows read-only is strict zero-grant** — not even the NUL device is writable; `> $null` redirection still works (documented in the backend package).
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-pwsh-sandbox
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
沙盒消费型的 [`ctx.bash` 执行器 seam](../bash/) 的 PowerShell 实现:每条命令以 `pwsh -NoLogo -NoProfile -NonInteractive -Command <command>` 运行,**经 `ctx.sandbox` 隔离**,选定模式、强制完整性、拒绝事实都盖在每次结算的结果上。它是 [`@deepseek-ai/dsh-bash-sandbox`](../bash-sandbox/) 的 pwsh 孪生,按 [pwsh 执行器与工具决策](../../../.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md) 逐调用镜像——隔离实体本身是平台无关的:Windows 上沙盒 seam 解析到 ACL 受限令牌 runner 链([`@deepseek-ai/dsh-sandbox-windows-acl`](../../sandbox/sandbox-windows-acl/)),Linux/macOS 上解析到 bwrap/Landlock/Seatbelt。
|
|
6
|
+
|
|
7
|
+
执行器继承 [`@deepseek-ai/dsh-pwsh-local`](../pwsh-local/) 的进程机制,并消费其 argv 级 seam(`argv()` / `runArgv()` / `startArgv()` / `onProcessDone()`)把精确的 pwsh 调用经 provider 包装。沙盒策略(模式 + 工作区根目录)不是本包的配置:每次调用由 `ctx.sandboxPolicy` 随行(工具层传调用会话解析后的策略;直接调用回退到部署策略)。
|
|
8
|
+
|
|
9
|
+
## 行为
|
|
10
|
+
|
|
11
|
+
- `danger-full-access`:命令经本地执行器原样运行;结果携带 `sandbox: { mode, denied: false }`。
|
|
12
|
+
- 受限模式(`read-only`、`workspace-write`):pwsh argv 由 `ctx.sandbox.confine()` 包装;runner 启动失败按 fail-closed 抛 `SANDBOX_UNAVAILABLE`(前台抛错、后台记 `runnerFailed` 事实),被拒绝的写按所选后端的 `denialSignatures` 分类为 `sandbox.denied`。
|
|
13
|
+
|
|
14
|
+
## 模型体验
|
|
15
|
+
|
|
16
|
+
### 隔离生效,拒绝以命令失败呈现
|
|
17
|
+
|
|
18
|
+
#### 模型看到什么
|
|
19
|
+
|
|
20
|
+
受限命令自身的 stderr(Windows ACL runner 下如 `Access to the path '...' is denied.`);工具层把分类后的拒绝转成标准权限拒绝面,与 bash 工具完全一致。
|
|
21
|
+
|
|
22
|
+
#### Token 影响
|
|
23
|
+
|
|
24
|
+
除命令 stderr 与工具层标准拒绝面外,无额外模型可见文本。
|
|
25
|
+
|
|
26
|
+
#### KV Cache 影响
|
|
27
|
+
|
|
28
|
+
无直接影响;拒绝呈现面属于工具层。
|
|
29
|
+
|
|
30
|
+
## 已知限制与后续工作
|
|
31
|
+
|
|
32
|
+
- **Windows 上读不受限**(ACL runner 只限写);读边界文档在 `@deepseek-ai/dsh-sandbox-windows-acl`。
|
|
33
|
+
- **Windows workspace-write 的临时区域是真实临时目录**(`GetTempPathW`)。这是有意为之的后端自定义选择,与 Landlock 的决策(`readWrite: ['/tmp', ...]`)同类:seam 的 "backend-defined temp area" 词汇表允许它,`tests/acl.e2e.ts` 的逃逸探针也正是因此位于 temp 树之外。按运行创建私有临时目录(bwrap `--tmpfs /tmp` 的语义)还需 runner 改写环境块——这是可选的进一步加固,而非正确性缺口。
|
|
34
|
+
- **Windows read-only 是严格零授权**——连 NUL 设备都不可写;`> $null` 重定向不受影响(后端包有文档)。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { SandboxUnavailableError } from "@deepseek-ai/dsh-sandbox";
|
|
2
|
+
import { PwshLocalExecutor } from "@deepseek-ai/dsh-pwsh-local";
|
|
3
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
4
|
+
//#region lib/types/helpers.js
|
|
5
|
+
/**
|
|
6
|
+
* Internal sandbox-result classification helpers — deliberate call-for-call
|
|
7
|
+
* mirror of `@deepseek-ai/dsh-bash-sandbox/src/helpers.ts` (the pwsh twin of
|
|
8
|
+
* the bash consumer shares the identical classification dialect).
|
|
9
|
+
*
|
|
10
|
+
* @module @deepseek-ai/dsh-pwsh-sandbox/helpers
|
|
11
|
+
*/
|
|
12
|
+
/** Node-local spawn codes proven to identify executable resolution or permission failure. */
|
|
13
|
+
const EXECUTABLE_SPAWN_CODES = new Set(["EACCES", "ENOENT"]);
|
|
14
|
+
/** Whether the caller-owned spawn cwd can be entered. */
|
|
15
|
+
function isUsableWorkdir(path) {
|
|
16
|
+
try {
|
|
17
|
+
if (!statSync(path).isDirectory()) return false;
|
|
18
|
+
accessSync(path, constants.X_OK);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Attribute only Node ENOENT/EACCES failures with positive argv[0] provenance
|
|
26
|
+
* after independently ruling out the caller-owned cwd. A supplied error path
|
|
27
|
+
* must exactly identify the runner; without one, the syscall must. With a
|
|
28
|
+
* usable cwd, these codes describe resolution or execute permission for that
|
|
29
|
+
* argv[0] or its shebang interpreter.
|
|
30
|
+
* The workdir is checked at classification time, not atomically with spawn;
|
|
31
|
+
* concurrent path replacement may change attribution but cannot permit an
|
|
32
|
+
* unconfined execution.
|
|
33
|
+
* @param error - the original spawn rejection.
|
|
34
|
+
* @param runnerProgram - provider argv[0], the executable that establishes confinement.
|
|
35
|
+
* @param workdir - the caller-owned spawn cwd, checked independently for usability.
|
|
36
|
+
* @returns whether the rejection has executable-specific runner evidence.
|
|
37
|
+
*/
|
|
38
|
+
function isRunnerSpawnFailure(error, runnerProgram, workdir) {
|
|
39
|
+
if (runnerProgram === void 0 || !isUsableWorkdir(workdir)) return false;
|
|
40
|
+
if (typeof error !== "object" || error === null) return false;
|
|
41
|
+
const { code, path, syscall } = error;
|
|
42
|
+
if (typeof code !== "string" || !EXECUTABLE_SPAWN_CODES.has(code)) return false;
|
|
43
|
+
if (typeof syscall !== "string") return false;
|
|
44
|
+
const exactSyscall = `spawn ${runnerProgram}`;
|
|
45
|
+
if (path === void 0) return syscall === exactSyscall;
|
|
46
|
+
if (typeof path !== "string" || path.length === 0 || path !== runnerProgram) return false;
|
|
47
|
+
return syscall === "spawn" || syscall === exactSyscall;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Classify a failed run against the selected backend's denial dialect.
|
|
51
|
+
* @param result - settled foreground run.
|
|
52
|
+
* @param signatures - case-insensitive denial substrings from the active wrap.
|
|
53
|
+
* @returns whether the failed run matches that denial dialect.
|
|
54
|
+
*/
|
|
55
|
+
function classifyDenial(result, signatures) {
|
|
56
|
+
return matchesSignature(result.exitCode, result.stderr.text, signatures);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Classify one settled process against the selected backend's structured
|
|
60
|
+
* runner-failure rules. Each rule requires a nonzero exit, its optional
|
|
61
|
+
* exit-code gate, and a fatal signature on one stderr line after exact
|
|
62
|
+
* informational lines are excluded.
|
|
63
|
+
* @param exitCode - process exit code; null means signal termination.
|
|
64
|
+
* @param stderr - collected stderr text, left unchanged.
|
|
65
|
+
* @param rules - structured runner-failure rules from the active wrap.
|
|
66
|
+
* @returns the first matching fatal line, or undefined when evidence is insufficient.
|
|
67
|
+
*/
|
|
68
|
+
function classifyRunnerFailure(exitCode, stderr, rules) {
|
|
69
|
+
if (exitCode === null || exitCode === 0) return void 0;
|
|
70
|
+
const lines = stderr.split(/\r?\n/);
|
|
71
|
+
for (const rule of rules) {
|
|
72
|
+
if (rule.allowedExitCodes !== void 0 && !rule.allowedExitCodes.includes(exitCode)) continue;
|
|
73
|
+
const informationalLines = new Set((rule.informationalLines ?? []).map((line) => line.toLowerCase()));
|
|
74
|
+
const fatalSignatures = rule.fatalSignatures.filter((signature) => signature.trim().length > 0).map((signature) => signature.toLowerCase());
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const lowered = line.toLowerCase();
|
|
77
|
+
if (informationalLines.has(lowered)) continue;
|
|
78
|
+
if (fatalSignatures.some((signature) => lowered.includes(signature))) return { detail: line };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Match a non-zero exit against case-insensitive stderr signatures.
|
|
84
|
+
* @param exitCode - process exit code; null means signal termination.
|
|
85
|
+
* @param stderr - collected stderr text.
|
|
86
|
+
* @param signatures - substrings identifying the selected backend's dialect.
|
|
87
|
+
* @returns whether this is a non-zero exit whose stderr matches a signature.
|
|
88
|
+
*/
|
|
89
|
+
function matchesSignature(exitCode, stderr, signatures) {
|
|
90
|
+
if (exitCode === null || exitCode === 0) return false;
|
|
91
|
+
const lowered = stderr.toLowerCase();
|
|
92
|
+
return signatures.some((signature) => lowered.includes(signature.toLowerCase()));
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region lib/types/index.js
|
|
96
|
+
/**
|
|
97
|
+
* Sandbox-consuming PowerShell executor — the pwsh twin of
|
|
98
|
+
* `@deepseek-ai/dsh-bash-sandbox`. It wraps the exact local pwsh argv through
|
|
99
|
+
* `ctx.sandbox` (which on Windows resolves to the ACL restricted-token runner
|
|
100
|
+
* chain), inherits local process mechanics, and reports the selected mode,
|
|
101
|
+
* enforcement, and denial facts. Positive runner-launch evidence means the
|
|
102
|
+
* command never ran: foreground calls throw `SANDBOX_UNAVAILABLE`, while
|
|
103
|
+
* background processes carry `runnerFailed`; other spawn rejections retain
|
|
104
|
+
* local-executor semantics. The tool layer owns the escalation approval flow
|
|
105
|
+
* through `ctx.approval`; this executor reports the sandbox facts the tool
|
|
106
|
+
* renders.
|
|
107
|
+
* @module @deepseek-ai/dsh-pwsh-sandbox
|
|
108
|
+
*/
|
|
109
|
+
/**
|
|
110
|
+
* Registers as `ctx.bash` in place of the local pwsh executor and requires a
|
|
111
|
+
* `ctx.sandbox` provider plus `ctx.sandboxPolicy`; the tool layer carries the
|
|
112
|
+
* sandbox denial rendering and escalation surface (see the
|
|
113
|
+
* pwsh-tool-and-executor Agent Note). Tool calls pass the calling session's
|
|
114
|
+
* resolved policy; direct calls fall back to deployment policy.
|
|
115
|
+
* `result.sandbox` reports the mode, enforcement, and denial facts the tool
|
|
116
|
+
* renders.
|
|
117
|
+
*/
|
|
118
|
+
var SandboxPwshExecutor = class extends PwshLocalExecutor {
|
|
119
|
+
static inject = [
|
|
120
|
+
"subprocess",
|
|
121
|
+
"sandbox",
|
|
122
|
+
"sandboxPolicy"
|
|
123
|
+
];
|
|
124
|
+
mode;
|
|
125
|
+
/**
|
|
126
|
+
* Per-process confinement facts retained until settlement. Providers may
|
|
127
|
+
* vary enforcement and diagnostic dialect between overlapping calls, so a
|
|
128
|
+
* shared latest-wrap value would classify a process against the wrong facts.
|
|
129
|
+
* Unconfined processes have no entry.
|
|
130
|
+
*/
|
|
131
|
+
processFacts = /* @__PURE__ */ new Map();
|
|
132
|
+
constructor(ctx, config) {
|
|
133
|
+
super(ctx, config);
|
|
134
|
+
this.mode = ctx.sandboxPolicy.defaultMode;
|
|
135
|
+
}
|
|
136
|
+
/** The configured default mode — the capability fact the tool layer reads. */
|
|
137
|
+
get sandboxMode() {
|
|
138
|
+
return this.mode;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Stamp a complete per-call policy onto the spec. Tool calls supply the
|
|
142
|
+
* calling session's resolved mode and root; lower-level callers fall back to
|
|
143
|
+
* the deployment policy.
|
|
144
|
+
*/
|
|
145
|
+
resolve(request) {
|
|
146
|
+
return {
|
|
147
|
+
...super.resolve(request),
|
|
148
|
+
sandboxPolicy: request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async run(spec) {
|
|
152
|
+
const policy = spec.sandboxPolicy;
|
|
153
|
+
const { mode } = policy;
|
|
154
|
+
if (mode === "danger-full-access") return {
|
|
155
|
+
...await super.run(spec),
|
|
156
|
+
sandbox: {
|
|
157
|
+
mode,
|
|
158
|
+
denied: false
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const confined = this.confine(spec, {
|
|
162
|
+
...policy,
|
|
163
|
+
mode
|
|
164
|
+
});
|
|
165
|
+
let result;
|
|
166
|
+
try {
|
|
167
|
+
result = await this.runArgv(spec, confined.argv);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (spec.signal?.aborted === true) spec.signal.throwIfAborted();
|
|
170
|
+
if (isRunnerSpawnFailure(error, confined.argv[0], spec.workdir)) throw new SandboxUnavailableError(mode, String(error));
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
const runnerFailure = classifyRunnerFailure(result.exitCode, result.stderr.text, confined.runnerFailureRules);
|
|
174
|
+
if (runnerFailure !== void 0) throw new SandboxUnavailableError(mode, runnerFailure.detail);
|
|
175
|
+
return {
|
|
176
|
+
...result,
|
|
177
|
+
sandbox: {
|
|
178
|
+
mode,
|
|
179
|
+
denied: classifyDenial(result, confined.denialSignatures),
|
|
180
|
+
enforcement: confined.enforcement
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
start(spec) {
|
|
185
|
+
const policy = spec.sandboxPolicy;
|
|
186
|
+
const { mode } = policy;
|
|
187
|
+
if (mode === "danger-full-access") return super.start(spec);
|
|
188
|
+
const confined = this.confine(spec, {
|
|
189
|
+
...policy,
|
|
190
|
+
mode
|
|
191
|
+
});
|
|
192
|
+
let proc;
|
|
193
|
+
try {
|
|
194
|
+
proc = this.startArgv(spec, confined.argv);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (isRunnerSpawnFailure(error, confined.argv[0], spec.workdir)) throw new SandboxUnavailableError(mode, String(error));
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
const { enforcement, denialSignatures, runnerFailureRules } = confined;
|
|
200
|
+
this.processFacts.set(proc, {
|
|
201
|
+
mode,
|
|
202
|
+
enforcement,
|
|
203
|
+
denialSignatures,
|
|
204
|
+
runnerFailureRules,
|
|
205
|
+
runnerProgram: confined.argv[0],
|
|
206
|
+
workdir: spec.workdir
|
|
207
|
+
});
|
|
208
|
+
return proc;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Stamp per-process sandbox facts before `done` settles. Full-access
|
|
212
|
+
* processes have no facts; signal deaths are not denials.
|
|
213
|
+
*/
|
|
214
|
+
onProcessDone(proc, stderr, spawnFailed, spawnError) {
|
|
215
|
+
const facts = this.processFacts.get(proc);
|
|
216
|
+
if (facts !== void 0) {
|
|
217
|
+
this.processFacts.delete(proc);
|
|
218
|
+
const runnerFailed = spawnFailed ? isRunnerSpawnFailure(spawnError, facts.runnerProgram, facts.workdir) : classifyRunnerFailure(proc.exitCode, stderr, facts.runnerFailureRules) !== void 0;
|
|
219
|
+
proc.sandbox = {
|
|
220
|
+
mode: facts.mode,
|
|
221
|
+
denied: !runnerFailed && matchesSignature(proc.exitCode, stderr, facts.denialSignatures),
|
|
222
|
+
enforcement: facts.enforcement,
|
|
223
|
+
...runnerFailed ? { runnerFailed } : {}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
super.onProcessDone(proc, stderr, spawnFailed, spawnError);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Wrap one pwsh invocation via the `ctx.sandbox` provider. Provider errors
|
|
230
|
+
* propagate unchanged; the returned argv is handed directly to the local
|
|
231
|
+
* executor's subprocess path.
|
|
232
|
+
* @param spec - resolved execution spec whose pwsh argv is confined.
|
|
233
|
+
* @param policy - resolved confined execution policy.
|
|
234
|
+
* @returns the provider's exact argv and settlement-classification facts.
|
|
235
|
+
*/
|
|
236
|
+
confine(spec, policy) {
|
|
237
|
+
return this.ctx.sandbox.confine(this.argv(spec), policy);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
export { SandboxPwshExecutor, SandboxPwshExecutor as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-pwsh-sandbox`.
|
|
4
|
+
* @module @deepseek-ai/dsh-pwsh-sandbox/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-pwsh-sandbox";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "pwsh-sandbox-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package exposes no independent event sequence or
|
|
13
|
+
* mutable data relation beyond contracts enforced at its owning seams.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal sandbox-result classification helpers — deliberate call-for-call
|
|
3
|
+
* mirror of `@deepseek-ai/dsh-bash-sandbox/src/helpers.ts` (the pwsh twin of
|
|
4
|
+
* the bash consumer shares the identical classification dialect).
|
|
5
|
+
*
|
|
6
|
+
* @module @deepseek-ai/dsh-pwsh-sandbox/helpers
|
|
7
|
+
*/
|
|
8
|
+
import type { BashRunResult } from '@deepseek-ai/dsh-bash';
|
|
9
|
+
import type { RunnerFailureRule } from '@deepseek-ai/dsh-sandbox';
|
|
10
|
+
/**
|
|
11
|
+
* Attribute only Node ENOENT/EACCES failures with positive argv[0] provenance
|
|
12
|
+
* after independently ruling out the caller-owned cwd. A supplied error path
|
|
13
|
+
* must exactly identify the runner; without one, the syscall must. With a
|
|
14
|
+
* usable cwd, these codes describe resolution or execute permission for that
|
|
15
|
+
* argv[0] or its shebang interpreter.
|
|
16
|
+
* The workdir is checked at classification time, not atomically with spawn;
|
|
17
|
+
* concurrent path replacement may change attribution but cannot permit an
|
|
18
|
+
* unconfined execution.
|
|
19
|
+
* @param error - the original spawn rejection.
|
|
20
|
+
* @param runnerProgram - provider argv[0], the executable that establishes confinement.
|
|
21
|
+
* @param workdir - the caller-owned spawn cwd, checked independently for usability.
|
|
22
|
+
* @returns whether the rejection has executable-specific runner evidence.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isRunnerSpawnFailure(error: unknown, runnerProgram: string | undefined, workdir: string): boolean;
|
|
25
|
+
/** Fatal runner evidence retained for infrastructure-error detail. */
|
|
26
|
+
interface RunnerFailureMatch {
|
|
27
|
+
/** The original stderr line that matched a fatal signature. */
|
|
28
|
+
detail: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Classify a failed run against the selected backend's denial dialect.
|
|
32
|
+
* @param result - settled foreground run.
|
|
33
|
+
* @param signatures - case-insensitive denial substrings from the active wrap.
|
|
34
|
+
* @returns whether the failed run matches that denial dialect.
|
|
35
|
+
*/
|
|
36
|
+
export declare function classifyDenial(result: BashRunResult, signatures: readonly string[]): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Classify one settled process against the selected backend's structured
|
|
39
|
+
* runner-failure rules. Each rule requires a nonzero exit, its optional
|
|
40
|
+
* exit-code gate, and a fatal signature on one stderr line after exact
|
|
41
|
+
* informational lines are excluded.
|
|
42
|
+
* @param exitCode - process exit code; null means signal termination.
|
|
43
|
+
* @param stderr - collected stderr text, left unchanged.
|
|
44
|
+
* @param rules - structured runner-failure rules from the active wrap.
|
|
45
|
+
* @returns the first matching fatal line, or undefined when evidence is insufficient.
|
|
46
|
+
*/
|
|
47
|
+
export declare function classifyRunnerFailure(exitCode: number | null, stderr: string, rules: readonly RunnerFailureRule[]): RunnerFailureMatch | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Match a non-zero exit against case-insensitive stderr signatures.
|
|
50
|
+
* @param exitCode - process exit code; null means signal termination.
|
|
51
|
+
* @param stderr - collected stderr text.
|
|
52
|
+
* @param signatures - substrings identifying the selected backend's dialect.
|
|
53
|
+
* @returns whether this is a non-zero exit whose stderr matches a signature.
|
|
54
|
+
*/
|
|
55
|
+
export declare function matchesSignature(exitCode: number | null, stderr: string, signatures: readonly string[]): boolean;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox-consuming PowerShell executor — the pwsh twin of
|
|
3
|
+
* `@deepseek-ai/dsh-bash-sandbox`. It wraps the exact local pwsh argv through
|
|
4
|
+
* `ctx.sandbox` (which on Windows resolves to the ACL restricted-token runner
|
|
5
|
+
* chain), inherits local process mechanics, and reports the selected mode,
|
|
6
|
+
* enforcement, and denial facts. Positive runner-launch evidence means the
|
|
7
|
+
* command never ran: foreground calls throw `SANDBOX_UNAVAILABLE`, while
|
|
8
|
+
* background processes carry `runnerFailed`; other spawn rejections retain
|
|
9
|
+
* local-executor semantics. The tool layer owns the escalation approval flow
|
|
10
|
+
* through `ctx.approval`; this executor reports the sandbox facts the tool
|
|
11
|
+
* renders.
|
|
12
|
+
* @module @deepseek-ai/dsh-pwsh-sandbox
|
|
13
|
+
*/
|
|
14
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash';
|
|
16
|
+
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
|
|
17
|
+
import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local';
|
|
18
|
+
import type { Config as LocalConfig } from '@deepseek-ai/dsh-pwsh-local';
|
|
19
|
+
/**
|
|
20
|
+
* Plugin config: the local executor's knobs, verbatim. The sandbox policy —
|
|
21
|
+
* the default mode and fallback `workspace-write` root — is NOT here: it lives
|
|
22
|
+
* on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), which resolves
|
|
23
|
+
* each calling session's mode and cwd for every enforcing capability. The
|
|
24
|
+
* runner choice is likewise the `ctx.sandbox` provider's config, not this
|
|
25
|
+
* executor's.
|
|
26
|
+
*/
|
|
27
|
+
export type Config = LocalConfig;
|
|
28
|
+
/**
|
|
29
|
+
* Registers as `ctx.bash` in place of the local pwsh executor and requires a
|
|
30
|
+
* `ctx.sandbox` provider plus `ctx.sandboxPolicy`; the tool layer carries the
|
|
31
|
+
* sandbox denial rendering and escalation surface (see the
|
|
32
|
+
* pwsh-tool-and-executor Agent Note). Tool calls pass the calling session's
|
|
33
|
+
* resolved policy; direct calls fall back to deployment policy.
|
|
34
|
+
* `result.sandbox` reports the mode, enforcement, and denial facts the tool
|
|
35
|
+
* renders.
|
|
36
|
+
*/
|
|
37
|
+
export declare class SandboxPwshExecutor extends PwshLocalExecutor {
|
|
38
|
+
static inject: string[];
|
|
39
|
+
private readonly mode;
|
|
40
|
+
/**
|
|
41
|
+
* Per-process confinement facts retained until settlement. Providers may
|
|
42
|
+
* vary enforcement and diagnostic dialect between overlapping calls, so a
|
|
43
|
+
* shared latest-wrap value would classify a process against the wrong facts.
|
|
44
|
+
* Unconfined processes have no entry.
|
|
45
|
+
*/
|
|
46
|
+
private readonly processFacts;
|
|
47
|
+
constructor(ctx: Context, config: Config);
|
|
48
|
+
/** The configured default mode — the capability fact the tool layer reads. */
|
|
49
|
+
get sandboxMode(): SandboxMode;
|
|
50
|
+
/**
|
|
51
|
+
* Stamp a complete per-call policy onto the spec. Tool calls supply the
|
|
52
|
+
* calling session's resolved mode and root; lower-level callers fall back to
|
|
53
|
+
* the deployment policy.
|
|
54
|
+
*/
|
|
55
|
+
resolve(request: BashExecRequest): BashExecSpec;
|
|
56
|
+
run(spec: BashExecSpec): Promise<BashRunResult>;
|
|
57
|
+
start(spec: BashExecSpec): BashProcess;
|
|
58
|
+
/**
|
|
59
|
+
* Stamp per-process sandbox facts before `done` settles. Full-access
|
|
60
|
+
* processes have no facts; signal deaths are not denials.
|
|
61
|
+
*/
|
|
62
|
+
protected onProcessDone(proc: BashProcess, stderr: string, spawnFailed: boolean, spawnError?: unknown): void;
|
|
63
|
+
/**
|
|
64
|
+
* Wrap one pwsh invocation via the `ctx.sandbox` provider. Provider errors
|
|
65
|
+
* propagate unchanged; the returned argv is handed directly to the local
|
|
66
|
+
* executor's subprocess path.
|
|
67
|
+
* @param spec - resolved execution spec whose pwsh argv is confined.
|
|
68
|
+
* @param policy - resolved confined execution policy.
|
|
69
|
+
* @returns the provider's exact argv and settlement-classification facts.
|
|
70
|
+
*/
|
|
71
|
+
private confine;
|
|
72
|
+
}
|
|
73
|
+
export default SandboxPwshExecutor;
|
|
74
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-pwsh-sandbox`.
|
|
3
|
+
* @module @deepseek-ai/dsh-pwsh-sandbox/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "pwsh-sandbox-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
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-pwsh-sandbox",
|
|
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.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/bash/pwsh-sandbox"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-bash": "^0.0.1-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
37
|
+
"@deepseek-ai/dsh-sandbox": "^0.0.1-rc.1",
|
|
38
|
+
"@deepseek-ai/dsh-pwsh-local": "^0.0.1-rc.1",
|
|
39
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.1",
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-bash": "^0.0.1-rc.1",
|
|
44
|
+
"@deepseek-ai/dsh-pwsh-local": "^0.0.1-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-sandbox": "^0.0.1-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-sandbox-local": "^0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.0.1-rc.1",
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
51
|
+
}
|
|
52
|
+
}
|