@mzzsfy/dsh-shell-select 0.1.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/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @mzzsfy/dsh-shell-select
2
+
3
+ Windows shell 链接管:禁用官方 `pwsh` 工具与执行器,替换为可配置多客户端的 `shell` 工具——pwsh / git bash / cmd / WSL / 任意第三方实现,自定义路径,多个客户端并存,设默认,模型经工具参数按名选择。
4
+
5
+ ## 工作方式
6
+
7
+ - bundle 补丁按 id 禁用官方 `tool-pwsh`、`pwsh-sandbox` 两行,插入本包主行(`ctx.shell` 执行器 + `shell` 工具 + 设置节 + 浏览器设置页路由)与 guard 哨兵行;卸载本包即补丁消失,官方行自动复位。
8
+ - client 半区 `dsh.client.inject` 声明(官方机制,dsh-client-file-upload 等官方包同款):保证 client 运行时模块表中 `primitives`/`slots` 面在场;primitives 另有 try/catch 降级自绘兜底。
9
+ - 本包执行器完整实现官方 `ctx.shell` seam(`resolve`/`run`/`start`/`runFor`/`startFor`/`entryFor`/`sandboxMode`),结构官方 PwshLocalExecutor 同构(实例状态/方法全公有——cordis 服务代理会把经 `ctx.shell` 调用的方法 `this` 重定向到阴影对象,`#` 私有触发品牌检查错误)。agent preset 注入的官方 `pwsh` 工具因此照常可用:经 `ctx.shell` 代理走默认客户端;`shell` 工具提供多客户端选择面。
10
+ - 沙箱语义官方同构:`danger-full-access` 直跑;受限模式经 `ctx.sandbox.confine` 包装并按官方方言分类拒绝/runner 失败,权限模型不变。
11
+ - 死态自愈:用户 patch 层禁用主行的窗口内,guard 哨兵(id 含 `/`,市场不写该层)代挂官方 `dsh-tool-pwsh` + `dsh-pwsh-sandbox` 恢复 host 面服务与 boot 组合(预载失败退避重试,至多 3 次),任一方复活先卸代挂。哨兵/复活让位语义逐项同构 `dsh-llm-pi-gateway`。边界:cordis 服务按 fiber 树解析,代挂对 agent preset 作用域不可见,死态窗口新会话的 preset tool-pwsh 行拒挂(报错清晰)属预期。
12
+ - POSIX 上本包全部行停用,官方 bash 链不受影响。
13
+
14
+ ## 工具
15
+
16
+ `shell` 工具(官方 `pwsh` 工具逐调用镜像):
17
+
18
+ - 参数:`command`、`description`、`shell`(客户端 id,缺省用默认)、`timeoutMs`、`workdir`、`run_in_background`,沙箱升权面与官方一致。
19
+ - 输出标记与官方逐字同构:`[exit code: N]`、`[killed by signal: X]`、`[timed out after …]`、`[sandbox: …]`、截断 spill 提示;terminal 卡退出 pill 照常复原。
20
+
21
+ ## 会话卡(tool.call.toolview)
22
+
23
+ 注册 keyed slot `tool.call.toolview`(key `shell`/`pwsh`/`bash` 三支,priority -1)替换默认通用卡,官方 `terminalCardModel` 同构派生(argsRaw + 结果文本尾部退出标记)。`pwsh`/`bash` 两支覆盖官方工具名:死态窗口 guard 代挂官方 tool-pwsh 时,其调用行同样获得增强卡;persistent 形(无 description)与后台 ack、isError、溢出预览一样回退简版行,不误渲染:
24
+
25
+ - 折叠行:状态点(失败红/中断黄/其余工具图标)+ `Shell` 标题 + 描述首行摘要,失败摘要红显。
26
+ - 展开卡:状态点 + cwd 末段目录 + 客户端名徽标(模型传了 `shell` 参数时)+ 失败 pill(退出码/信号)+ 复制命令/复制输出按钮 + 命令折行带行号 + 输出区(横向滚动,竖向限高)。
27
+ - 非终端意图回退简版行:摘要 + 可展开原文 + 检查按钮。
28
+ - 图标取官方 `dsh-client-ui-primitives`(StateDot/IconApi/IconInspect/Chevron),模块表缺席时降级自绘。
29
+
30
+ ## 配置(settings 命名空间 `shell-select`)
31
+
32
+ ```yaml
33
+ shell-select:
34
+ shells:
35
+ - id: pwsh
36
+ name: PowerShell
37
+ kind: pwsh # pwsh | bash | cmd | wsl
38
+ path: "" # 留空自动探测;bash 形探测序含 msys2 锚位(见下)
39
+ args: [] # 留空用形态默认;自定义模板以 {command} 为命令占位
40
+ login: false # bash 形 true 时 -lc 登录壳(-lc 只取 /usr/bin;/mingw64/bin 需另配 env MSYSTEM=MINGW64)
41
+ distro: "" # wsl 形发行版(如 Ubuntu-22.04),留空用 wsl 默认;args 模板条目忽略
42
+ env: {} # 条目级环境变量,如 { MSYSTEM: MINGW64 };wsl 形键自动经 WSLENV 透传
43
+ - id: git-bash
44
+ name: Git Bash
45
+ kind: bash
46
+ path: ""
47
+ - id: cmd
48
+ name: CMD
49
+ kind: cmd
50
+ path: ""
51
+ default: pwsh
52
+ ```
53
+
54
+ 出厂默认即上述三客户端;浏览器「设置 → Shell 管理」页可增删改、探测路径、扫描本机、编辑发行版与环境变量(环境编辑态每行一条 `K=V`)。第三方实现:任意可执行文件 + 自定义 args 模板。
55
+
56
+ env 优先级(同键高右):内置覆盖集(NO_COLOR/PAGER/GIT_PAGER)< 条目 `env` < 调用方环境(spec.env + 会话 `DSH_*` 事实)。wsl 形把条目 env 与调用方环境全部键(WSLENV 本身除外)追加进 WSLENV 白名单,过 WSL 边界不静默失效。
57
+
58
+ ### 命令黑名单(deny)
59
+
60
+ - 配置节顶层 `deny`:正则字符串数组,对模型提交的整条命令文本匹配(大小写不敏感),命中即拒绝——**deny 绝对,无豁免语义**;空数组 = 不拦截(出厂默认)。
61
+ - 精细放行在模式内用正则前瞻表达:`deny: ['rm -rf\\s+(?!\\S*node_modules)']` 禁 rm -rf 但放行清依赖目录;默认放行系统下独立 allow 列表的唯一语义就是覆盖 deny,与"deny 绝对"矛盾,故不设。
62
+ - 拦截点在执行器 runFor/startFor 入口:经 `ctx.shell` 代理的官方 pwsh 工具同样受管。拒绝以模型可见标记返回:`[blocked by shell-select: matches deny pattern …]`,后台任务在启动前同步拒绝,不产生僵尸任务。
63
+ - 定位是防误触护栏而非安全边界:整文本正则挡不住间接包装(编码/嵌套壳),真正的边界始终是访问模式与沙箱。坏正则条目容错跳过,不瘫执行链;设置页多行文本域编辑(每行一条正则),保存时校验正则合法性。
64
+
65
+ ### bash 形探测与 msys2
66
+
67
+ - 候选序:`Program Files\Git\bin` → `Program Files (x86)\Git\bin` → `LocalAppData\Programs\Git\bin` → `C:\msys64\usr\bin\bash.exe` → `C:\msys64\bin\bash.exe` → PATH 扫描。
68
+ - `msys2.exe` 在管道 stdio 下静默失败(exit 0 零字节),永不入候选;System32\bash.exe 是 WSL 转发器,PATH 扫描一律排除 SystemRoot 下条目。
69
+ - msys2 条目建议配 `login: true`(`-lc` 登录壳拉起 `/etc/profile`)与 `env: { MSYSTEM: MINGW64 }`(启用 /mingw64/bin 的 gcc/make);git-bash 无需 login(自带 PATH)。
70
+ - wsl 形:`wsl [--exec | -d <distro> --exec] bash -c {command}`;条目 env 与调用方环境全部键(WSLENV 本身除外)经 WSLENV 追加透传,继承的 WSLENV 条目保留、显式配置项优先。
71
+
72
+ ## 已知边界
73
+
74
+ - **受限模式 × Cygwin 系运行时**(git-bash/msys2):confine 包装后 Cygwin mmap 在受限令牌下崩溃(`CreateFileMapping … fatal error`,exit 256)。命令失败可见、错误自解释,模型可换客户端或升权重跑;危险模式与 pwsh/cmd/wsl 不受影响。与 dsh-bash-terminal-ts 观察一致,属 Cygwin 运行时限制而非本包缺陷。
75
+ - wsl 形在未安装发行版的机器上:wsl.exe 输出 UTF-16 帮助文本,呈现为乱码;正常发行版下 bash 输出走管道为 UTF-8 不受影响。
76
+ - 竞品来源主张的评审结论:不受配置清单约束的模型自选任意终端未采纳(本包的按名选择以设置页清单为边界,越界即报配置指引,官方升权面已覆盖安全需求);不受限 bash 危害面不做;交互式 PTY 终端本轮不做,列入路线图后续立项。
77
+
78
+ ## 组合
79
+
80
+ ```yaml
81
+ - insert:
82
+ - id: shell-select
83
+ name: '@mzzsfy/dsh-shell-select'
84
+ - id: shell-select/guard
85
+ name: '@mzzsfy/dsh-shell-select/guard'
86
+ ```
87
+
88
+ 测试:`node --test "test/*.test.mjs"`(包目录内)。
@@ -0,0 +1,30 @@
1
+ # shell-select bundle patch:Windows shell 链零感知接管(同构 dsh-llm-pi-gateway)。
2
+ # 官方 tool-pwsh 与 pwsh-sandbox 行按 id 禁用,本包主行提供 ctx.shell 执行器、
3
+ # shell 工具、settings 节与浏览器半区路由;卸载本 bundle 即 patch 消失,官方行
4
+ # 自动复位。POSIX 官方 bash 链(tool-bash/bash-sandbox)一律不碰。
5
+ #
6
+ # 平台门控:本包全部 insert 行在非 win32 停用(host 半区 apply 内另有平台早退,
7
+ # 双重防护)。官方行禁用两平台皆写 true:POSIX 上这两行本就被 dsh-base 平台
8
+ # 表达式停用,重复写 true 为幂等;绝不可写 false,否则会在 POSIX 强行启用。
9
+ #
10
+ # 死态窗口:市场 UI 开关只即时禁用主行(bundle 移出 bundles 仅下次 boot 生效),
11
+ # 窗口内官方行仍被本 patch 禁用——guard 行(id 含 "/",市场行写入对其拒绝)
12
+ # 探测死态后代挂官方插件自愈,任一方复活先卸代挂。
13
+ #
14
+ # 哨兵自愈与复活让位语义逐项同构 packages/dsh-llm-pi-gateway 的 guard/takeover,
15
+ # 差异仅两处:官方代挂对象为 tool-pwsh + pwsh-sandbox 两行;行配置读官方
16
+ # `shell` settings 节(pwsh-sandbox 经其 Config 消费,保持官方 pwshPath 配置)。
17
+
18
+ - id: pwsh-sandbox
19
+ disabled: true
20
+
21
+ - id: tool-pwsh
22
+ disabled: true
23
+
24
+ - insert:
25
+ - id: shell-select
26
+ name: '@mzzsfy/dsh-shell-select'
27
+ disabled: !!js process.platform !== 'win32'
28
+ - id: shell-select/guard
29
+ name: '@mzzsfy/dsh-shell-select/guard'
30
+ disabled: !!js process.platform !== 'win32'
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@mzzsfy/dsh-shell-select",
3
+ "description": "Windows shell 接管:替换默认 pwsh 工具为可配置多客户端的 shell 工具(git bash/WSL/cmd/pwsh,自定义路径,默认客户端,模型按名选择)",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "src/executor.mjs",
7
+ "exports": {
8
+ ".": "./src/executor.mjs",
9
+ "./guard": "./src/guard.js",
10
+ "./client": "./src/client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "dsh": {
14
+ "bundle": {
15
+ "patch": "./cordis.patch.yml"
16
+ },
17
+ "client": {
18
+ "platform": "web",
19
+ "inject": [
20
+ "@deepseek-ai/dsh-client-runtime",
21
+ "@deepseek-ai/dsh-client-locale",
22
+ "@deepseek-ai/dsh-client-ui-slots",
23
+ "@deepseek-ai/dsh-client-ui-primitives",
24
+ "@deepseek-ai/dsh-client-ui-conversation"
25
+ ]
26
+ }
27
+ },
28
+ "scripts": {
29
+ "test": "node --test \"test/*.test.mjs\"",
30
+ "npmPublish": "npm publish --access public --provenance=false --registry=https://registry.npmjs.org"
31
+ },
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/mzzsfy/dsh-plugin.git"
36
+ },
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
40
+ "peerDependencies": {
41
+ "@deepseek-ai/cordis": "^4.0.2",
42
+ "@deepseek-ai/dsh-http-proxy": ">=0.1.5-rc.2",
43
+ "@deepseek-ai/dsh-jobs": ">=0.1.5-rc.2",
44
+ "@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
45
+ "@deepseek-ai/dsh-sandbox": ">=0.1.5-rc.2",
46
+ "@deepseek-ai/dsh-shell": ">=0.1.5-rc.2",
47
+ "@deepseek-ai/dsh-subprocess": ">=0.1.5-rc.2",
48
+ "@deepseek-ai/dsh-timeout": ">=0.1.5-rc.2",
49
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
50
+ "@deepseek-ai/schemastery": "^3.18.2"
51
+ },
52
+ "devDependencies": {
53
+ "@deepseek-ai/cordis": "4.0.2",
54
+ "@deepseek-ai/dsh-http-proxy": "0.1.5-rc.2",
55
+ "@deepseek-ai/dsh-jobs": "0.1.5-rc.2",
56
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
57
+ "@deepseek-ai/dsh-sandbox": "0.1.5-rc.2",
58
+ "@deepseek-ai/dsh-shell": "0.1.5-rc.2",
59
+ "@deepseek-ai/dsh-subprocess": "0.1.5-rc.2",
60
+ "@deepseek-ai/dsh-timeout": "0.1.5-rc.2",
61
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.2",
62
+ "@deepseek-ai/schemastery": "3.18.2"
63
+ },
64
+ "files": [
65
+ "src",
66
+ "test",
67
+ "cordis.patch.yml",
68
+ "README.md"
69
+ ]
70
+ }
package/src/api.mjs ADDED
@@ -0,0 +1,109 @@
1
+ // 浏览器半区路由:'/api/shell-select/*',形态照 dsh-maintain(具名 exact 路由 +
2
+ // 方法/跨源守卫 + 业务异常归一 400)。webServer 缺席(headless)时跳过,不影响
3
+ // shell 能力本体。faces 由执行器注入,路由层无自有状态。
4
+
5
+ // 路由前缀
6
+ const API_ROOT = '/api/shell-select'
7
+
8
+ function sendJson(res, status, payload) {
9
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
10
+ res.end(JSON.stringify(payload))
11
+ }
12
+
13
+ // 同源守卫:浏览器写请求恒带 Origin,与 Host 不符即拒(官方 maintain 同构)
14
+ function rejectCrossOrigin(req, res) {
15
+ const origin = req.headers ? req.headers.origin : undefined
16
+ if (!origin) return false
17
+ const host = String((req.headers && req.headers.host) || '')
18
+ let sameOrigin = false
19
+ try {
20
+ sameOrigin = host.length > 0 && new URL(origin).host === host.toLowerCase()
21
+ } catch {
22
+ sameOrigin = false
23
+ }
24
+ if (sameOrigin) return false
25
+ sendJson(res, 403, { error: '跨源请求被拒绝' })
26
+ return true
27
+ }
28
+
29
+ const route = (method, handler) => async (req, res) => {
30
+ if (req.method !== method) {
31
+ sendJson(res, 405, { error: 'method not allowed' })
32
+ return
33
+ }
34
+ if (method === 'POST' && rejectCrossOrigin(req, res)) return
35
+ try {
36
+ await handler(req, res)
37
+ } catch (error) {
38
+ sendJson(res, 400, { error: error && error.message ? error.message : String(error) })
39
+ }
40
+ }
41
+
42
+ function readBody(req) {
43
+ const BODY_MAX_BYTES = 64 * 1024
44
+ return new Promise((resolveBody, rejectBody) => {
45
+ let size = 0
46
+ const chunks = []
47
+ req.on('data', (chunk) => {
48
+ size += chunk.length
49
+ if (size > BODY_MAX_BYTES) {
50
+ rejectBody(new Error('请求体超过上限'))
51
+ req.destroy()
52
+ return
53
+ }
54
+ chunks.push(chunk)
55
+ })
56
+ req.on('end', () => resolveBody(Buffer.concat(chunks).toString('utf8')))
57
+ req.on('error', rejectBody)
58
+ })
59
+ }
60
+
61
+ /**
62
+ * 挂载配置/探测路由。可写操作(settings.update)在 settings 缺席时由闭包抛错归一 400。
63
+ * @param {object} ctx cordis context
64
+ * @param {{listShells: Function, readConfig: Function, updateConfig: Function, detect: Function, probe: Function}} faces
65
+ */
66
+ export function mountRoutes(ctx, faces) {
67
+ const webServer = ctx.get('webServer')
68
+ if (webServer === undefined || typeof webServer.register !== 'function') {
69
+ ctx.logger?.warn?.('shell-select: webServer 服务不在场(headless 部署),设置页路由跳过;可用 settings.yaml 配置 shell-select 节')
70
+ return
71
+ }
72
+ const routes = [
73
+ {
74
+ // exact 路由以 path 为唯一键,GET/POST 在同一 handler 内分流
75
+ path: `${API_ROOT}/config`,
76
+ handler: async (req, res) => {
77
+ if (req.method === 'GET') {
78
+ sendJson(res, 200, { ...faces.readConfig(), resolved: faces.listShells() })
79
+ return
80
+ }
81
+ await route('POST', async (req2, res2) => {
82
+ const patch = JSON.parse(await readBody(req2))
83
+ if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) throw new Error('请求体必须是对象')
84
+ const next = await faces.updateConfig(patch)
85
+ sendJson(res2, 200, { ok: true, resolved: next })
86
+ })(req, res)
87
+ },
88
+ },
89
+ {
90
+ path: `${API_ROOT}/probe`,
91
+ handler: route('POST', async (req, res) => {
92
+ const { path: candidatePath } = JSON.parse(await readBody(req))
93
+ if (typeof candidatePath !== 'string' || candidatePath.length === 0) throw new Error('path 必须是非空字符串')
94
+ sendJson(res, 200, { exists: faces.probe(candidatePath) })
95
+ }),
96
+ },
97
+ {
98
+ path: `${API_ROOT}/detect`,
99
+ handler: route('POST', async (req, res) => {
100
+ const body = JSON.parse(await readBody(req))
101
+ const kinds = Array.isArray(body?.kinds) ? body.kinds : undefined
102
+ sendJson(res, 200, { found: faces.detect(kinds) })
103
+ }),
104
+ },
105
+ ]
106
+ for (const item of routes) {
107
+ ctx.effect(() => webServer.register({ kind: 'exact', path: item.path, handler: item.handler }), `shell-select ${item.path}`)
108
+ }
109
+ }
@@ -0,0 +1,21 @@
1
+ // apply 生命周期旗标:guard 据此识别本行功能性停摆。
2
+ // pending = apply 进行中或中途崩溃;active = 全部装配落定;inactive = 声明式早退。
3
+ // 中途崩溃停留 pending,guard 对 pending 保守不代挂(不越权重启他人)。
4
+
5
+ let state = 'pending'
6
+
7
+ export function beginShellSelectApply() {
8
+ state = 'pending'
9
+ }
10
+
11
+ export function endShellSelectApplyActive() {
12
+ state = 'active'
13
+ }
14
+
15
+ export function endShellSelectApplyInactive() {
16
+ state = 'inactive'
17
+ }
18
+
19
+ export function shellSelectApplyState() {
20
+ return state
21
+ }