dsh-minimal-ptc 0.4.0
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 +21 -0
- package/README.en.md +60 -0
- package/README.md +73 -0
- package/cordis.patch.yml +8 -0
- package/lib/index.js +66 -0
- package/package.json +58 -0
- package/presets/ptc-minimal/agent.cordis.yml +246 -0
- package/presets/ptc-minimal/gitbash-executor.mjs +340 -0
- package/presets/ptc-minimal/preset.yml +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stardustlc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# dsh-minimal-ptc
|
|
2
|
+
|
|
3
|
+
> Minimal prompt x full PTC capabilities — a cleaner coding agent.
|
|
4
|
+
> Installing gives you a new agent mode: **Minimal PTC**, with Git Bash kept alive on Windows so the bash tool matches the RL training distribution.
|
|
5
|
+
|
|
6
|
+
## Highlights
|
|
7
|
+
|
|
8
|
+
- **RL-aligned**: the whole system prompt is one sentence — `You are a helpful software engineer assistant.` — close to the compact instruction distribution used during RL fine-tuning, without long-prompt format bias or context noise.
|
|
9
|
+
- **Complete toolset**: inherits the full PTC toolset — Code Mode SDK multi-step orchestration, files and search, Shell, Skills, plan mode, goals, subagents, and workflows.
|
|
10
|
+
- **We / Let's reasoning**: the Code Mode SDK packs thinking and doing into one TypeScript program, orchestrating multi-step operations before executing them once. This matches the high-scoring Minimal trajectory in DeepSeek's official Project2 V4.1b runs (99/96), where reasoning is dominated by `we` / `let's` instead of the standard-like `let me` / `I` blocks seen at 91/92.
|
|
11
|
+
- **Git Bash on Windows**: ships a preset-local Git Bash executor (GIT_BASH -> Program Files\Git -> LOCALAPPDATA\Git -> PATH). The bash tool is no longer disabled on Windows; pwsh remains available as a fallback.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
1. Add this package to a web profile (`package.json`):
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"dependencies": { "dsh-minimal-ptc": "^0.4.0" },
|
|
19
|
+
"dsh": { "profile": { "bundles": [..., "dsh-minimal-ptc"] } }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Local development can use a link: `"dsh-minimal-ptc": "link:E://deepseek//dsh-minimal-ptc"`.
|
|
23
|
+
|
|
24
|
+
2. Run `pnpm install` in the profile directory.
|
|
25
|
+
|
|
26
|
+
3. Restart the web profile process. The host row materializes the bundled preset into `$DSH_HOME/.agent-presets/ptc-minimal`.
|
|
27
|
+
|
|
28
|
+
4. Select **Minimal PTC** when starting a new session.
|
|
29
|
+
|
|
30
|
+
## Capabilities
|
|
31
|
+
|
|
32
|
+
- Web search is enabled via `tool-web`; arbitrary page fetch is disabled by default (`fetch: false`).
|
|
33
|
+
- Subagents are enabled: `subagent`, `subagent_fork`, `subagent_control`, workflows, and Ralph.
|
|
34
|
+
- `subagent_codex` and `subagent_claude_code` providers are disabled by default; remove their `disabled: true` lines to expose them.
|
|
35
|
+
|
|
36
|
+
## Windows Git Bash configuration
|
|
37
|
+
|
|
38
|
+
The preset-local Git Bash executor can be overridden in `agent.cordis.yml` under `gitbash-executor`:
|
|
39
|
+
|
|
40
|
+
| Option | Default | Meaning |
|
|
41
|
+
| :-- | :-- | :-- |
|
|
42
|
+
| `shellPath` | auto-detected | Fixed Git Bash path (e.g. `C:\\Program Files\\Git\\bin\\bash.exe`) |
|
|
43
|
+
| `timeoutMs` | 120000 | Per-command timeout (ms) |
|
|
44
|
+
| `maxTimeoutMs` | 600000 | Maximum timeout a request may ask for |
|
|
45
|
+
| `maxOutputBytes` | 64000 | Per-call output cap |
|
|
46
|
+
| `maxSpillBytes` | 67108864 | Output spill-to-disk cap |
|
|
47
|
+
| `graceMs` | 3000 | Grace period after timeout |
|
|
48
|
+
|
|
49
|
+
Detection order: `GIT_BASH` -> `Program Files\\Git` -> `Program Files (x86)\\Git` -> `LOCALAPPDATA\\Programs\\Git` -> PATH.
|
|
50
|
+
|
|
51
|
+
## Materialization policy
|
|
52
|
+
|
|
53
|
+
- No target directory -> write all preset files and a version marker.
|
|
54
|
+
- Marker version lower than the plugin version -> refresh from the bundled files.
|
|
55
|
+
- Directory exists without a marker (treated as user-created) -> leave it untouched.
|
|
56
|
+
- Upgrading: bump `version` in `package.json` and the `VERSION` constant in `lib/index.js`.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# dsh-minimal-ptc
|
|
2
|
+
|
|
3
|
+
> 极简提示词 × PTC 全能力 —— 一个更"干净"的编码 Agent。
|
|
4
|
+
> 安装即获得新的 Agent 模式:**极简 PTC 模式**;Windows 自动启用 Git Bash,bash 工具与 RL 训练分布对齐。
|
|
5
|
+
|
|
6
|
+
## 卖点
|
|
7
|
+
|
|
8
|
+
- **对齐 RL 训练**:系统提示词只有一句 `You are a helpful software engineer assistant.`,
|
|
9
|
+
贴近 RL 微调时的简洁指令分布,没有长提示词带来的格式偏置与上下文噪音,
|
|
10
|
+
让模型按训练时的节奏干活。
|
|
11
|
+
- **工具完整**:完整继承 PTC 模式的全部工具 —— Code Mode SDK 多步编排、
|
|
12
|
+
文件读写与检索、Shell、Skills、计划、目标、子代理、工作流,一个不少。
|
|
13
|
+
- **We / Let's 思维链**:Code Mode SDK 把"想"和"做"装进一个 TypeScript 程序,
|
|
14
|
+
多步操作先编排、再一次执行。这与 DeepSeek 官方跑分环境(Project2 V4.1b)中
|
|
15
|
+
Minimal 高分轨迹一致:99/96 的两跑以 `we` / `let's` 为主,而不是
|
|
16
|
+
Standard/PTC 常见的 `let me` / `I` 长块。
|
|
17
|
+
- **Windows 保持 bash**:预设内置 Git Bash executor(自动探测 GIT_BASH → Program Files\Git → LOCALAPPDATA\Git → PATH),`bash` 工具不再在 Windows 上被禁用;pwsh 仍保留为备用。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
1. 把本包加入 web profile(`package.json`):
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
"dependencies": { "dsh-minimal-ptc": "^0.4.0" },
|
|
25
|
+
"dsh": { "profile": { "bundles": [..., "dsh-minimal-ptc"] } }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
本地开发也可以用 link:`"dsh-minimal-ptc": "link:E://deepseek//dsh-minimal-ptc"`。
|
|
29
|
+
|
|
30
|
+
2. 在 profile 目录执行 `pnpm install`。
|
|
31
|
+
|
|
32
|
+
3. 重启 web profile 进程:宿主行会把内置预设物化到
|
|
33
|
+
`$DSH_HOME/.agent-presets/ptc-minimal`。
|
|
34
|
+
|
|
35
|
+
4. 新建会话时选择 **极简 PTC 模式**。
|
|
36
|
+
|
|
37
|
+
## Windows Git Bash 配置
|
|
38
|
+
|
|
39
|
+
预设内置的 Git Bash executor 可在 `agent.cordis.yml` 的 `gitbash-executor` 行覆盖:
|
|
40
|
+
|
|
41
|
+
| 配置 | 默认 | 说明 |
|
|
42
|
+
| :-- | :-- | :-- |
|
|
43
|
+
| `shellPath` | 自动探测 | 固定 Git Bash 路径(如 `C:\\Program Files\\Git\\bin\\bash.exe`) |
|
|
44
|
+
| `timeoutMs` | 120000 | 单次命令超时(毫秒) |
|
|
45
|
+
| `maxTimeoutMs` | 600000 | 请求可要求的最大超时 |
|
|
46
|
+
| `maxOutputBytes` | 64000 | 单次输出上限 |
|
|
47
|
+
| `maxSpillBytes` | 67108864 | 输出溢出落盘上限 |
|
|
48
|
+
| `graceMs` | 3000 | 超时后的宽限时间 |
|
|
49
|
+
|
|
50
|
+
自动探测顺序:`GIT_BASH` → `Program Files\\Git` → `Program Files (x86)\\Git` → `LOCALAPPDATA\\Programs\\Git` → PATH。
|
|
51
|
+
|
|
52
|
+
## 目录结构
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
dsh-minimal-ptc/
|
|
56
|
+
├── cordis.patch.yml # 插入宿主行(物化预设)
|
|
57
|
+
├── lib/index.js # 宿主插件:物化 ptc-minimal 预设到用户预设根目录
|
|
58
|
+
└── presets/ptc-minimal/ # 内置 Agent 模式(极简提示词 + PTC 全能力)
|
|
59
|
+
├── agent.cordis.yml
|
|
60
|
+
├── gitbash-executor.mjs # Windows Git Bash 探测与执行器(零依赖)
|
|
61
|
+
└── preset.yml
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 说明
|
|
65
|
+
|
|
66
|
+
- 物化策略:目标目录不存在 → 写入全部文件并留版本标记;版本标记低于当前
|
|
67
|
+
版本 → 刷新;目录存在但无标记(用户自建)→ 不覆盖。
|
|
68
|
+
- 升级插件:改 `version` 并同步 `lib/index.js` 里的 `VERSION` 常量即可刷新
|
|
69
|
+
用户根目录里的预设文件。
|
|
70
|
+
|
|
71
|
+
## 许可证
|
|
72
|
+
|
|
73
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// dsh-minimal-ptc 宿主行:把内置的 ptc-minimal 预设物化到用户预设根目录。
|
|
2
|
+
//
|
|
3
|
+
// 预设发现是实时重读的,因此安装完成后重启一次 web profile 进程,
|
|
4
|
+
// 选择器里就会出现「极简 PTC 模式」。
|
|
5
|
+
//
|
|
6
|
+
// 物化策略:目标目录不存在 → 写入全部文件并留版本标记;
|
|
7
|
+
// 版本标记低于当前版本 → 用插件自带文件刷新;
|
|
8
|
+
// 目录存在但无标记(视为用户自建)→ 不覆盖。
|
|
9
|
+
//
|
|
10
|
+
// 零运行时依赖:harness home 的解析逻辑内置(DSH_HOME 环境变量优先,
|
|
11
|
+
// 否则 ~/.dsh),与 dsh 官方规则一致。
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
13
|
+
import { homedir } from 'node:os'
|
|
14
|
+
import { join, resolve } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
|
|
17
|
+
export const name = 'ptc-minimal-preset'
|
|
18
|
+
|
|
19
|
+
const VERSION = '0.4.0'
|
|
20
|
+
const PRESET_ID = 'ptc-minimal'
|
|
21
|
+
const MARKER = '.dsh-ptc-minimal.version'
|
|
22
|
+
|
|
23
|
+
const packageDir = fileURLToPath(new URL('..', import.meta.url))
|
|
24
|
+
const presetSourceDir = join(packageDir, 'presets', PRESET_ID)
|
|
25
|
+
|
|
26
|
+
/** 与 @deepseek-ai/dsh-home-paths 相同的解析规则:DSH_HOME > ~/.dsh。 */
|
|
27
|
+
function dshHomePath(...segments) {
|
|
28
|
+
const env = process.env.DSH_HOME
|
|
29
|
+
const fromEnv = typeof env === 'string' && env.trim().length > 0 ? env : null
|
|
30
|
+
let base = fromEnv ?? join(homedir(), '.dsh')
|
|
31
|
+
if (base === '~') base = homedir()
|
|
32
|
+
if (base.startsWith('~/') || base.startsWith('~\\')) base = join(homedir(), base.slice(2))
|
|
33
|
+
return resolve(base, ...segments)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function materialize(logger) {
|
|
37
|
+
const dir = dshHomePath('.agent-presets', PRESET_ID)
|
|
38
|
+
mkdirSync(dir, { recursive: true })
|
|
39
|
+
|
|
40
|
+
let marker = null
|
|
41
|
+
try {
|
|
42
|
+
marker = readFileSync(join(dir, MARKER), 'utf8').trim()
|
|
43
|
+
} catch {
|
|
44
|
+
// 无标记:可能是首次安装,也可能是用户自建目录
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (marker === VERSION) return
|
|
48
|
+
if (marker === null && existsSync(join(dir, 'agent.cordis.yml'))) {
|
|
49
|
+
logger?.warn(`dsh-minimal-ptc: ${dir} exists without our marker; leaving it alone`)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const file of ['agent.cordis.yml', 'preset.yml', 'gitbash-executor.mjs']) {
|
|
54
|
+
writeFileSync(join(dir, file), readFileSync(join(presetSourceDir, file)))
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(join(dir, MARKER), VERSION + '\n')
|
|
57
|
+
logger?.info(`dsh-minimal-ptc: materialized agent preset "${PRESET_ID}" into ${dir}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function apply(ctx) {
|
|
61
|
+
try {
|
|
62
|
+
materialize(ctx.logger)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
ctx.logger?.warn(`dsh-minimal-ptc: preset materialization failed: ${String(error)}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-minimal-ptc",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Minimal prompt x full PTC tools for DeepSeek Harness - aligned with the Project2 we/let's trajectory (Minimal 99/96).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"presets",
|
|
17
|
+
"cordis.patch.yml",
|
|
18
|
+
"README.md",
|
|
19
|
+
"README.en.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"dsh": {
|
|
23
|
+
"bundle": {
|
|
24
|
+
"patch": "./cordis.patch.yml"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"dsh",
|
|
29
|
+
"deepseek-harness",
|
|
30
|
+
"plugin",
|
|
31
|
+
"agent-preset",
|
|
32
|
+
"code-mode",
|
|
33
|
+
"dsh-plugin",
|
|
34
|
+
"deepseek",
|
|
35
|
+
"llm-agent",
|
|
36
|
+
"minimal",
|
|
37
|
+
"ptc",
|
|
38
|
+
"we-lets",
|
|
39
|
+
"chain-of-thought"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"author": "stardustlc",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/STARDUSTLC666/dsh-minimal-ptc.git"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/STARDUSTLC666/dsh-minimal-ptc/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/STARDUSTLC666/dsh-minimal-ptc#readme",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "node --test"
|
|
56
|
+
},
|
|
57
|
+
"packageManager": "pnpm@11.7.0"
|
|
58
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# The `ptc-minimal` agent preset: the `code` (PTC 模式) capabilities with the
|
|
2
|
+
# `minimal` (极简模式) prompt.
|
|
3
|
+
#
|
|
4
|
+
# Everything in `code` is here unchanged — full standard capabilities plus the
|
|
5
|
+
# Code Mode tool presentation (`tool-presentation` with `mode: code`), where the
|
|
6
|
+
# model composes a TypeScript program against a generated SDK and `run_code`
|
|
7
|
+
# executes it. What differs is the persona row, taken verbatim from `minimal`:
|
|
8
|
+
# a fixed one-line persona marked `complete: true` with
|
|
9
|
+
# `includeRuntimeContext: false`, so global identity, Web orientation, tool
|
|
10
|
+
# guidance, and later assembly listeners cannot add prompt text, and runtime
|
|
11
|
+
# context snapshots are suppressed. The `agent-instructions` row from `code` is
|
|
12
|
+
# dropped for the same reason — its workspace instruction baselines would add
|
|
13
|
+
# user-role text the minimal prompt policy excludes.
|
|
14
|
+
#
|
|
15
|
+
# This file is an AGENT-PLANE composition. It is mounted under one agent's
|
|
16
|
+
# scope context, so every tool it registers belongs to that session alone.
|
|
17
|
+
# A service row here MUST sit inside a group carrying an `isolate` realm.
|
|
18
|
+
|
|
19
|
+
# ── identity ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
# The persona is the complete system prompt (minimal-mode prompt policy).
|
|
22
|
+
- id: persona
|
|
23
|
+
name: '@deepseek-ai/dsh-persona'
|
|
24
|
+
config:
|
|
25
|
+
text: You are a helpful software engineer assistant.
|
|
26
|
+
complete: true
|
|
27
|
+
includeRuntimeContext: false
|
|
28
|
+
|
|
29
|
+
# ── shell ───────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
|
32
|
+
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`. Both shell tools consume the host
|
|
33
|
+
# registry from here; their executors are host-plane too.
|
|
34
|
+
- id: tool-bash
|
|
35
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
36
|
+
disabled: !!js process.platform === 'win32'
|
|
37
|
+
|
|
38
|
+
- id: tool-pwsh
|
|
39
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
40
|
+
disabled: !!js process.platform !== 'win32'
|
|
41
|
+
|
|
42
|
+
# Windows Git Bash shell stack. The preset-local executor runs every `bash`
|
|
43
|
+
# call through Git for Windows bash, keeping the model-facing tool name and
|
|
44
|
+
# stateless semantics identical to the POSIX `tool-bash` row above. Only
|
|
45
|
+
# mounted on win32; the POSIX host keeps the official bash runner.
|
|
46
|
+
- id: gitbash-shell
|
|
47
|
+
name: cordis:group
|
|
48
|
+
group: true
|
|
49
|
+
disabled: !!js process.platform !== 'win32'
|
|
50
|
+
isolate:
|
|
51
|
+
shell: true
|
|
52
|
+
config:
|
|
53
|
+
- id: gitbash-executor
|
|
54
|
+
name: ./gitbash-executor.mjs
|
|
55
|
+
config:
|
|
56
|
+
# 留空:executor 自动探测 GIT_BASH → 常见安装目录 → PATH → 'bash'
|
|
57
|
+
# 如需固定路径可显式写 shellPath: 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
58
|
+
timeoutMs: 120000
|
|
59
|
+
maxTimeoutMs: 600000
|
|
60
|
+
maxOutputBytes: 64000
|
|
61
|
+
maxSpillBytes: 67108864
|
|
62
|
+
graceMs: 3000
|
|
63
|
+
|
|
64
|
+
- id: tool-bash-win
|
|
65
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
66
|
+
config:
|
|
67
|
+
enableRunInBackground: false
|
|
68
|
+
|
|
69
|
+
# ── filesystem ──────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
# Both register into the host `tools` registry and provide nothing, so
|
|
72
|
+
# they need no realm. The `fs` service and its policy stay in the host.
|
|
73
|
+
- id: tool-fs
|
|
74
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
75
|
+
|
|
76
|
+
- id: tool-fs-search
|
|
77
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
78
|
+
config:
|
|
79
|
+
sampleOverCapGlobResults: false
|
|
80
|
+
|
|
81
|
+
# ── background jobs ────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
# Only the model-facing controls. The task REGISTRY stays on the host plane.
|
|
84
|
+
- id: tool-jobs
|
|
85
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
86
|
+
|
|
87
|
+
# ── skills ──────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
# The skill REGISTRY lives in the host composition and is layered per scope:
|
|
90
|
+
# these rows register into THIS preset's layer of it, so they need no realm.
|
|
91
|
+
- id: skill-filesystem
|
|
92
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
93
|
+
|
|
94
|
+
- id: tool-skill
|
|
95
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
96
|
+
|
|
97
|
+
# ── goals ───────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
# Only the model-facing tool. The goal SERVICE, its session driver, and the
|
|
100
|
+
# `/goal` command stay on the host plane.
|
|
101
|
+
- id: tool-goal
|
|
102
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
103
|
+
|
|
104
|
+
# ── plan mode ───────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
# Plan state is per-agent by nature, so an entry-local realm is not a
|
|
107
|
+
# workaround here — it is the correct lifetime.
|
|
108
|
+
- id: planning
|
|
109
|
+
name: cordis:group
|
|
110
|
+
group: true
|
|
111
|
+
isolate:
|
|
112
|
+
planMode: true
|
|
113
|
+
config:
|
|
114
|
+
- id: plan-mode
|
|
115
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
116
|
+
config:
|
|
117
|
+
section: |
|
|
118
|
+
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
119
|
+
|
|
120
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
121
|
+
|
|
122
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
123
|
+
|
|
124
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
125
|
+
|
|
126
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
127
|
+
|
|
128
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
129
|
+
|
|
130
|
+
# ── compaction ──────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner
|
|
133
|
+
# must share this realm rather than sit outside it. `tokenMeter` stays HOST-side.
|
|
134
|
+
- id: compaction
|
|
135
|
+
name: cordis:group
|
|
136
|
+
group: true
|
|
137
|
+
isolate:
|
|
138
|
+
compaction: true
|
|
139
|
+
toolResultPruner: true
|
|
140
|
+
config:
|
|
141
|
+
- id: compaction-basic
|
|
142
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
143
|
+
|
|
144
|
+
- id: command-compact
|
|
145
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
146
|
+
|
|
147
|
+
- id: tool-result-pruner
|
|
148
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
149
|
+
config:
|
|
150
|
+
thresholdChars: 8192
|
|
151
|
+
headChars: 4096
|
|
152
|
+
tailChars: 1024
|
|
153
|
+
|
|
154
|
+
# ── delegation and workflows ────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
# The `subagents` registry and its spawn/fork backends live in the HOST
|
|
157
|
+
# composition. This preset contributes the delegation TOOLS. `workflows` shares
|
|
158
|
+
# one entry-local realm here, and a consumer left outside would resolve a host
|
|
159
|
+
# registry this preset does not populate.
|
|
160
|
+
- id: delegation
|
|
161
|
+
name: cordis:group
|
|
162
|
+
group: true
|
|
163
|
+
isolate:
|
|
164
|
+
workflowEngine: true
|
|
165
|
+
config:
|
|
166
|
+
- id: tool-subagent-control
|
|
167
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
168
|
+
|
|
169
|
+
- id: tool-subagent-list-agents
|
|
170
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
171
|
+
|
|
172
|
+
- id: tool-subagent
|
|
173
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
174
|
+
config:
|
|
175
|
+
provider: spawn
|
|
176
|
+
toolName: subagent
|
|
177
|
+
backgroundMode: continuable
|
|
178
|
+
|
|
179
|
+
- id: tool-subagent-fork
|
|
180
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
181
|
+
config:
|
|
182
|
+
provider: fork
|
|
183
|
+
toolName: subagent_fork
|
|
184
|
+
backgroundMode: continuable
|
|
185
|
+
|
|
186
|
+
# Product providers are host-plane singletons. Remove `disabled` from
|
|
187
|
+
# either ordinary tool row to expose that product to agents on this preset.
|
|
188
|
+
- id: tool-subagent-codex
|
|
189
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
190
|
+
disabled: true
|
|
191
|
+
config:
|
|
192
|
+
provider: codex
|
|
193
|
+
toolName: subagent_codex
|
|
194
|
+
enableRunInBackground: false
|
|
195
|
+
maxDepth: provider-managed
|
|
196
|
+
|
|
197
|
+
- id: tool-subagent-claude-code
|
|
198
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
199
|
+
disabled: true
|
|
200
|
+
config:
|
|
201
|
+
provider: claude-code
|
|
202
|
+
toolName: subagent_claude_code
|
|
203
|
+
enableRunInBackground: false
|
|
204
|
+
maxDepth: provider-managed
|
|
205
|
+
|
|
206
|
+
- id: workflow-worker-thread
|
|
207
|
+
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
|
208
|
+
config:
|
|
209
|
+
provider: spawn
|
|
210
|
+
|
|
211
|
+
- id: tool-workflow
|
|
212
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
213
|
+
|
|
214
|
+
- id: tool-ralph
|
|
215
|
+
name: '@deepseek-ai/dsh-tool-ralph'
|
|
216
|
+
config:
|
|
217
|
+
subagentProvider: spawn
|
|
218
|
+
maxRounds: 64
|
|
219
|
+
|
|
220
|
+
# ── remaining model-facing rows ─────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
- id: tool-ask-user
|
|
223
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
224
|
+
|
|
225
|
+
- id: tool-todo
|
|
226
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
227
|
+
config:
|
|
228
|
+
allowParallelInProgress: true
|
|
229
|
+
|
|
230
|
+
# The `web` service and its search provider stay in the host composition; only
|
|
231
|
+
# the model-facing tool is per-session.
|
|
232
|
+
- id: tool-web
|
|
233
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
234
|
+
config:
|
|
235
|
+
fetch: false
|
|
236
|
+
searchTimeoutMs: 60000
|
|
237
|
+
|
|
238
|
+
# ── presentation ────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
# Code Mode for this agent alone. The row waits for the host's `codeRuntime`
|
|
241
|
+
# rather than assuming it: a deployment that composes no TypeScript runtime
|
|
242
|
+
# fails this preset at mount, naming this id, instead of at the first request.
|
|
243
|
+
- id: tool-presentation
|
|
244
|
+
name: '@deepseek-ai/dsh-agent-tool-presentation'
|
|
245
|
+
config:
|
|
246
|
+
mode: code
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitbash-executor: Git-for-Windows bash provider for the `minimal-gitbash`
|
|
3
|
+
* agent preset.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists:
|
|
6
|
+
* - the shipped `minimal` preset's persistent PTY shell cannot start on
|
|
7
|
+
* Windows (`@deepseek-ai/dsh-subprocess-local` refuses win32 terminal
|
|
8
|
+
* inspection), and
|
|
9
|
+
* - `@deepseek-ai/dsh-bash-local` hardcodes `bash` from PATH, which a
|
|
10
|
+
* Windows machine does not provide.
|
|
11
|
+
*
|
|
12
|
+
* This plugin replaces `ctx.shell` inside an entry-local realm (see the
|
|
13
|
+
* preset's `gitbash-shell` group) and runs every command as
|
|
14
|
+
* `"<git bash>" -c <command>` through the host subprocess service — a fresh
|
|
15
|
+
* shell per call, with the same `bash` tool name and `str_replace_editor`
|
|
16
|
+
* surface as the original preset. Only Node built-in modules are imported:
|
|
17
|
+
* preset-local files resolve against the preset directory, so DSH package
|
|
18
|
+
* imports (node_modules) would fail, while `node:fs` is always available.
|
|
19
|
+
*
|
|
20
|
+
* Sandbox note: the MSYS runtime cannot initialize inside the Windows
|
|
21
|
+
* restricted-token sandbox (it cannot create its signal pipes), so commands
|
|
22
|
+
* are gated on the danger-full-access policy. The bash tool's single-call
|
|
23
|
+
* `sandbox_permissions: "danger-full-access"` escalation, or switching the
|
|
24
|
+
* session to full access, satisfies the gate.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync } from 'node:fs'
|
|
28
|
+
|
|
29
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
30
|
+
export const name = 'gitbash-executor'
|
|
31
|
+
|
|
32
|
+
/** Required host services; ordering matters because apply() reads them eagerly. */
|
|
33
|
+
export const inject = ['subprocess', 'sandboxPolicy']
|
|
34
|
+
|
|
35
|
+
/** Node's maximum timer delay before setTimeout clamps to 1ms. */
|
|
36
|
+
const MAX_TIMER_DELAY_MS = 2147483647
|
|
37
|
+
|
|
38
|
+
const ENV_OVERRIDES = {
|
|
39
|
+
NO_COLOR: '1',
|
|
40
|
+
TERM: 'dumb',
|
|
41
|
+
PAGER: 'cat',
|
|
42
|
+
GIT_PAGER: 'cat',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert a Git Bash / MSYS drive path such as `/d/foo` into the Windows path
|
|
47
|
+
* `D:\foo` that Node's child_process can use as cwd or executable path.
|
|
48
|
+
* Only the single-letter drive form is converted: `/d`, `/d/`, `/d/foo`
|
|
49
|
+
* (the letter must be immediately followed by `/` or end the string), so
|
|
50
|
+
* MSYS root paths like `/usr/bin` are left untouched instead of being
|
|
51
|
+
* mangled into `U:\sr\bin`. Everything else (UNC, `D:\`, `D:/`) passes
|
|
52
|
+
* through unchanged.
|
|
53
|
+
*/
|
|
54
|
+
export function toWindowsPath(value) {
|
|
55
|
+
if (process.platform !== 'win32' || typeof value !== 'string' || value.length === 0) {
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
const match = /^\/\s*([A-Za-z])(?:$|\/(.*))$/.exec(value)
|
|
59
|
+
if (match === null) return value
|
|
60
|
+
const drive = `${match[1].toUpperCase()}:`
|
|
61
|
+
const rest = match[2] ?? ''
|
|
62
|
+
if (rest === '') return `${drive}\\`
|
|
63
|
+
return `${drive}\\${rest.replace(/\//g, '\\')}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Candidates for the Git-for-Windows bash executable, in preference order:
|
|
68
|
+
* the GIT_BASH environment variable, the standard install roots, this
|
|
69
|
+
* machine's known install location, then every `bash.exe` found on PATH.
|
|
70
|
+
*/
|
|
71
|
+
function shellPathCandidates(env) {
|
|
72
|
+
const candidates = [
|
|
73
|
+
env.GIT_BASH,
|
|
74
|
+
env.ProgramFiles === undefined ? undefined : `${env.ProgramFiles}\\Git\\bin\\bash.exe`,
|
|
75
|
+
env['ProgramFiles(x86)'] === undefined ? undefined : `${env['ProgramFiles(x86)']}\\Git\\bin\\bash.exe`,
|
|
76
|
+
env.LOCALAPPDATA === undefined ? undefined : `${env.LOCALAPPDATA}\\Programs\\Git\\bin\\bash.exe`,
|
|
77
|
+
]
|
|
78
|
+
if (typeof env.PATH === 'string' && env.PATH.length > 0) {
|
|
79
|
+
for (const dir of env.PATH.split(';')) {
|
|
80
|
+
if (dir.length === 0) continue
|
|
81
|
+
candidates.push(`${dir}\\bash.exe`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return candidates
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the shell executable to spawn. An explicit configuration wins; a
|
|
89
|
+
* POSIX host always gets `bash`. On Windows the first existing candidate is
|
|
90
|
+
* used (GIT_BASH → install roots → PATH), falling back to the bare `bash`
|
|
91
|
+
* name when nothing exists so spawn reports a resolution error.
|
|
92
|
+
*/
|
|
93
|
+
export function detectShellPath(explicit, env = process.env) {
|
|
94
|
+
if (process.platform !== 'win32') {
|
|
95
|
+
return typeof explicit === 'string' && explicit.length > 0 ? explicit : 'bash'
|
|
96
|
+
}
|
|
97
|
+
if (typeof explicit === 'string' && explicit.length > 0) return toWindowsPath(explicit)
|
|
98
|
+
for (const candidate of shellPathCandidates(env)) {
|
|
99
|
+
if (typeof candidate !== 'string' || candidate.length === 0) continue
|
|
100
|
+
if (existsSync(candidate)) return toWindowsPath(candidate)
|
|
101
|
+
}
|
|
102
|
+
return 'bash'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function positiveNumber(config, label, fallback) {
|
|
106
|
+
const value = config[label] ?? fallback
|
|
107
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
108
|
+
throw new TypeError(`${name}: ${label} must be a positive finite number`)
|
|
109
|
+
}
|
|
110
|
+
return value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveConfig(config, env = process.env) {
|
|
114
|
+
const source = config ?? {}
|
|
115
|
+
const timeoutMs = positiveNumber(source, 'timeoutMs', 120000)
|
|
116
|
+
const maxTimeoutMs = positiveNumber(source, 'maxTimeoutMs', 600000)
|
|
117
|
+
const graceMs = positiveNumber(source, 'graceMs', 3000)
|
|
118
|
+
if (timeoutMs > MAX_TIMER_DELAY_MS || maxTimeoutMs > MAX_TIMER_DELAY_MS || graceMs > MAX_TIMER_DELAY_MS) {
|
|
119
|
+
throw new TypeError(`${name}: timeoutMs, maxTimeoutMs and graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`)
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
shellPath: detectShellPath(source.shellPath, env),
|
|
123
|
+
cwd: typeof source.cwd === 'string' && source.cwd.length > 0
|
|
124
|
+
? toWindowsPath(source.cwd)
|
|
125
|
+
: undefined,
|
|
126
|
+
timeoutMs,
|
|
127
|
+
maxTimeoutMs,
|
|
128
|
+
maxOutputBytes: positiveNumber(source, 'maxOutputBytes', 64000),
|
|
129
|
+
maxSpillBytes: positiveNumber(source, 'maxSpillBytes', 64 * 1024 * 1024),
|
|
130
|
+
graceMs,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Fuse upstream cancellation with an identifiable timeout. */
|
|
135
|
+
function timeoutSignal(upstream, timeoutMs) {
|
|
136
|
+
const timer = new AbortController()
|
|
137
|
+
const id = setTimeout(() => {
|
|
138
|
+
timer.abort(new Error('BASH_TIMEOUT'))
|
|
139
|
+
}, timeoutMs)
|
|
140
|
+
const signal = upstream === undefined
|
|
141
|
+
? timer.signal
|
|
142
|
+
: AbortSignal.any([upstream, timer.signal])
|
|
143
|
+
return {
|
|
144
|
+
signal,
|
|
145
|
+
timedOut: () => timer.signal.aborted,
|
|
146
|
+
dispose: () => clearTimeout(id),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Project a settled subprocess collector into the tool result shape. */
|
|
151
|
+
function finalOutput(reader) {
|
|
152
|
+
const read = reader.readFrom(0)
|
|
153
|
+
return {
|
|
154
|
+
text: read.text,
|
|
155
|
+
truncated: read.lossy,
|
|
156
|
+
...read.spillPath === undefined ? {} : { spillPath: read.spillPath },
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Wrap a spawn failure with the shell path and workdir that caused it. */
|
|
161
|
+
function spawnError(shellPath, workdir, cause) {
|
|
162
|
+
return new Error(`${name}: failed to start ${shellPath} (workdir: ${workdir}): ${cause?.message ?? String(cause)}`, {
|
|
163
|
+
cause,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Register the git-bash executor as the entry-local `shell` provider. */
|
|
168
|
+
export function apply(ctx, config) {
|
|
169
|
+
const resolved = resolveConfig(config)
|
|
170
|
+
const subprocess = ctx.get('subprocess')
|
|
171
|
+
if (subprocess === undefined) {
|
|
172
|
+
throw new Error(`${name}: ctx.subprocess is unavailable`)
|
|
173
|
+
}
|
|
174
|
+
const sandboxPolicy = ctx.get('sandboxPolicy')
|
|
175
|
+
|
|
176
|
+
const gateMessage = (mode) =>
|
|
177
|
+
`${name}: the Git-for-Windows bash runtime cannot start inside the "${mode ?? 'unknown'}" sandbox `
|
|
178
|
+
+ '(the MSYS runtime cannot create its signal pipes under the restricted token). '
|
|
179
|
+
+ 'Retry this exact command once with sandbox_permissions: "danger-full-access" plus a justification, '
|
|
180
|
+
+ 'or ask the user to switch the session sandbox mode to full access.'
|
|
181
|
+
|
|
182
|
+
const spawnSpec = (spec, argv, stdoutMaxBytes, signal) => ({
|
|
183
|
+
argv,
|
|
184
|
+
cwd: spec.workdir,
|
|
185
|
+
stdio: {
|
|
186
|
+
stdin: spec.stdin !== undefined ? { data: spec.stdin } : 'ignore',
|
|
187
|
+
stdout: { maxBytes: stdoutMaxBytes, spill: { maxBytes: resolved.maxSpillBytes } },
|
|
188
|
+
stderr: { maxBytes: resolved.maxOutputBytes, spill: { maxBytes: resolved.maxSpillBytes } },
|
|
189
|
+
},
|
|
190
|
+
graceMs: resolved.graceMs,
|
|
191
|
+
signal,
|
|
192
|
+
env: {
|
|
193
|
+
...ENV_OVERRIDES,
|
|
194
|
+
...spec.env,
|
|
195
|
+
...spec.dshEnv,
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const spawnShell = (spec, stdoutMaxBytes, signal) => {
|
|
200
|
+
try {
|
|
201
|
+
return subprocess.spawn(spawnSpec(
|
|
202
|
+
spec,
|
|
203
|
+
[resolved.shellPath, '-c', spec.command],
|
|
204
|
+
stdoutMaxBytes,
|
|
205
|
+
signal,
|
|
206
|
+
))
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw spawnError(resolved.shellPath, spec.workdir, error)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const executor = {
|
|
213
|
+
/** Advertise confinement so the bash tool offers per-call escalation. */
|
|
214
|
+
get sandboxMode() {
|
|
215
|
+
return sandboxPolicy === undefined ? undefined : sandboxPolicy.defaultMode
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
resolve(request) {
|
|
219
|
+
const timeoutMs = Math.min(request.timeoutMs ?? resolved.timeoutMs, resolved.maxTimeoutMs)
|
|
220
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
221
|
+
throw new Error(`${name}: request.timeoutMs must be a positive finite number`)
|
|
222
|
+
}
|
|
223
|
+
const stdoutMaxBytes = request.stdoutMaxBytes ?? resolved.maxOutputBytes
|
|
224
|
+
if (!Number.isFinite(stdoutMaxBytes) || stdoutMaxBytes <= 0) {
|
|
225
|
+
throw new Error(`${name}: request.stdoutMaxBytes must be a positive finite number`)
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
command: request.command,
|
|
229
|
+
workdir: toWindowsPath(request.workdir ?? resolved.cwd ?? process.cwd()),
|
|
230
|
+
timeoutMs,
|
|
231
|
+
stdoutMaxBytes,
|
|
232
|
+
...request.signal === undefined ? {} : { signal: request.signal },
|
|
233
|
+
...request.stdin === undefined ? {} : { stdin: request.stdin },
|
|
234
|
+
...request.env === undefined ? {} : { env: request.env },
|
|
235
|
+
...request.dshEnv === undefined ? {} : { dshEnv: request.dshEnv },
|
|
236
|
+
sandboxPolicy: request.sandboxPolicy ?? (sandboxPolicy?.resolve === undefined ? undefined : sandboxPolicy.resolve()),
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async run(spec) {
|
|
241
|
+
const mode = spec.sandboxPolicy?.mode
|
|
242
|
+
// `undefined` means the deployment carries no sandbox policy at all.
|
|
243
|
+
if (mode !== undefined && mode !== 'danger-full-access') {
|
|
244
|
+
throw new Error(gateMessage(mode))
|
|
245
|
+
}
|
|
246
|
+
const fused = timeoutSignal(spec.signal, spec.timeoutMs)
|
|
247
|
+
try {
|
|
248
|
+
const handle = spawnShell(spec, spec.stdoutMaxBytes, fused.signal)
|
|
249
|
+
let outcome
|
|
250
|
+
try {
|
|
251
|
+
outcome = await handle.done
|
|
252
|
+
} catch (error) {
|
|
253
|
+
throw spawnError(resolved.shellPath, spec.workdir, error)
|
|
254
|
+
}
|
|
255
|
+
const stdout = finalOutput(handle.collected.stdout)
|
|
256
|
+
const stderr = finalOutput(handle.collected.stderr)
|
|
257
|
+
const timedOut = fused.timedOut()
|
|
258
|
+
const aborted = spec.signal !== undefined && spec.signal.aborted && !timedOut
|
|
259
|
+
return {
|
|
260
|
+
...outcome,
|
|
261
|
+
timedOut,
|
|
262
|
+
aborted,
|
|
263
|
+
timeoutMs: spec.timeoutMs,
|
|
264
|
+
stdout,
|
|
265
|
+
stderr,
|
|
266
|
+
...mode === undefined ? {} : { sandbox: { mode, denied: false } },
|
|
267
|
+
}
|
|
268
|
+
} finally {
|
|
269
|
+
fused.dispose()
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
start(spec) {
|
|
274
|
+
const mode = spec.sandboxPolicy?.mode
|
|
275
|
+
if (mode !== undefined && mode !== 'danger-full-access') {
|
|
276
|
+
throw new Error(gateMessage(mode))
|
|
277
|
+
}
|
|
278
|
+
const fused = timeoutSignal(spec.signal, spec.timeoutMs)
|
|
279
|
+
let running
|
|
280
|
+
try {
|
|
281
|
+
running = spawnShell(spec, resolved.maxOutputBytes, fused.signal)
|
|
282
|
+
} catch (error) {
|
|
283
|
+
fused.dispose()
|
|
284
|
+
throw error
|
|
285
|
+
}
|
|
286
|
+
const collected = {
|
|
287
|
+
stdout: running.collected.stdout,
|
|
288
|
+
stderr: running.collected.stderr,
|
|
289
|
+
}
|
|
290
|
+
let spawnFailureNote
|
|
291
|
+
const consumeSpawnFailure = () => {
|
|
292
|
+
const note = spawnFailureNote ?? ''
|
|
293
|
+
spawnFailureNote = undefined
|
|
294
|
+
return note
|
|
295
|
+
}
|
|
296
|
+
let stdoutOffset = 0
|
|
297
|
+
let stderrOffset = 0
|
|
298
|
+
const proc = {
|
|
299
|
+
status: 'running',
|
|
300
|
+
exitCode: null,
|
|
301
|
+
signal: null,
|
|
302
|
+
done: running.done.then((outcome) => {
|
|
303
|
+
if (proc.status === 'running') {
|
|
304
|
+
proc.status = fused.timedOut() || spec.signal?.aborted === true || outcome.signal !== null
|
|
305
|
+
? 'killed'
|
|
306
|
+
: 'completed'
|
|
307
|
+
}
|
|
308
|
+
proc.exitCode = outcome.exitCode
|
|
309
|
+
proc.signal = outcome.signal
|
|
310
|
+
}, (error) => {
|
|
311
|
+
proc.status = 'killed'
|
|
312
|
+
spawnFailureNote = spawnError(resolved.shellPath, spec.workdir, error).message
|
|
313
|
+
}).finally(() => fused.dispose()),
|
|
314
|
+
readOutput: () => {
|
|
315
|
+
const out = collected.stdout.readFrom(stdoutOffset)
|
|
316
|
+
const err = collected.stderr.readFrom(stderrOffset)
|
|
317
|
+
stdoutOffset = out.nextOffset
|
|
318
|
+
stderrOffset = err.nextOffset
|
|
319
|
+
const errText = err.text.length > 0 ? err.text : consumeSpawnFailure()
|
|
320
|
+
const separator = out.text.length > 0 && !out.text.endsWith('\n') ? '\n' : ''
|
|
321
|
+
return {
|
|
322
|
+
delta: out.text + (errText.length > 0 ? `${separator}[stderr]\n${errText}` : ''),
|
|
323
|
+
lossy: out.lossy || err.lossy,
|
|
324
|
+
...out.spillPath === undefined ? {} : { stdoutSpillPath: out.spillPath },
|
|
325
|
+
...err.spillPath === undefined ? {} : { stderrSpillPath: err.spillPath },
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
kill: () => {
|
|
329
|
+
if (proc.status !== 'running') return false
|
|
330
|
+
proc.status = 'killed'
|
|
331
|
+
running.terminate()
|
|
332
|
+
return true
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
return proc
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
ctx.provide('shell', executor)
|
|
340
|
+
}
|