dsh-shell-surfaces

DSH PLUGIN会话与消息

仓库 Nesarf/dsh-shell-surfaces

会话与消息 L4 · 沙盒安装通过 安全审查:低风险 可信度:银 可安装插件
安装这个插件

dsh-shell-surfaces 是 Nesarf 维护的 会话与消息 类 DSH 可安装插件, 仓库为 Nesarf/dsh-shell-surfaces,许可证 MIT。 下面按「装什么、怎么装、验没验过、跑起来要注意什么」逐项列出。

一句话选型结论

按资料字段整理

dsh-shell-surfaces 是会话与消息类的 DSH 可安装插件,验证等级 L4 · 沙盒安装通过,安全审查为低风险,有可直接复制的安装命令。验证通过不等于安全:插件会读写文件、执行命令或联网,装前先到仓库确认它做了什么;涉及密钥或系统权限的,建议先在隔离环境试装。

分类会话与消息 收录类型可安装插件 验证等级L4 · 沙盒安装通过 安全审查低风险
验证等级 L4 L4 · 沙盒安装通过
GitHub 星标 0 复刻 0 · 未关闭 issue 0
最近更新 2026-10-26 创建于 2026-10-14
README 47.5 KB 正文体量,越大说明说明书写得越细

安装

可安装插件
在 DSH 里执行,装完用 dsh plugin list 确认已挂上;标了 L4 / L5 的命令已在沙盒里真实跑通
dsh plugin --profile web add github:Nesarf/dsh-shell-surfaces#v1.0.0
  1. 1
    确认版本与运行环境

    仓库没有标注兼容版本与 Node.js 要求,先确认你的 DSH 版本能装,再继续。

  2. 2
    执行安装命令

    在 DSH 里执行上面的命令;多包仓库要注意区分具体子包名,别装错包。

  3. 3
    确认已挂载

    用 dsh plugin list 查看列表里是否出现这个插件,出现即挂载成功。

  4. 4
    试跑并检查权限

    加载后跑一个最小用例,确认它能正常工作;同时看它申请了哪些文件、命令或网络权限。

兼容 DSH 版本未标注版本不匹配时安装会报错
Node.js 要求未标注来自仓库 package.json 的 engines 字段
前置依赖无需要同时安装的同级依赖
npm 包名未发布到 npm可用 npm view 查看包信息

能做什么

会话与消息
字段来自项目仓库与公开索引,中文说明由本站标注

适用场景

终端命令

上游标签

bashdeepseek-harnessdshdsh-pluginfishmsys2nushellptyshellterminalwslzsh

验证等级与安全审查

L4 · 沙盒安装通过
自动化流程结论,不是人工审核
验证等级L4 · 沙盒安装通过安装命令在隔离沙盒内真实执行成功,无依赖错误
安全审查低风险静态分析结论,未标注不代表已通过审查
仓库健康度活跃按仓库活跃度与最近更新时间给出
可信度银综合仓库活跃度与验证结果

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 验证等级 L4:在沙盒里真实装过,依赖能装上,不是纸面声明。
  • 静态安全审查为低风险,没有发现明显可疑行为。
  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有声明 Node.js 版本要求,版本不匹配可能装不上。
  • 验证通过不等于安全:插件会读写文件、执行命令或联网,装前先到仓库确认它做了什么。

更适合

  • 需要在会话与消息场景里给 DSH 补能力的人。
  • 想装完就能用、不想自己排依赖的人。
  • 能接受在本地装依赖、愿意自己确认权限的人。

不太适合

  • 只使用 DSH 官方内置能力、不接受第三方插件的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
插件验证等级安全审查星标兼容版本安装命令
dsh-shell-surfaces当前 L4 · 沙盒安装通过 低风险 0 未标注 有
dsh-desktop L3 · 安装规范通过 低风险 3.0 万 未标注 无
openpets L4 · 沙盒安装通过 低风险 1,274 未标注 有
deeptide L4 · 沙盒安装通过 低风险 1,104 0.1.1-rc.2 有
jingyun-dsh L3 · 安装规范通过 低风险 1,004 未标注 有
study-mate 未做沙盒验证 未标注 856 未标注 无

仓库档案

会话与消息
字段来自项目仓库与公开索引,中文说明由本站标注
作者 / 维护方Nesarf仓库 dsh-shell-surfaces · 默认分支 main
分类会话与消息desktop
收录类型可安装插件声明了 dsh.bundle、可用 dsh plugin add 安装的条目
开发语言未标注仓库主语言
许可证MIT商用或二次分发前请确认条款原文
星标 / 复刻 / issue0 / 0 / 0收录时的数值,非实时
创建 / 更新2026-10-14 · 2026-10-26最后推送 2026-10-26
本站收录2026-10-30首次进入本站目录的时间

README 全文

47.5 KB
作者原文,中文版优先;只做安全转义,不改内容

dsh-shell-surfaces


https://github.com/Nesarf/dsh-shell-surfaces -- a DSH plugin.

dsh plugin --profile  add github:Nesarf/dsh-shell-surfaces          # the tip of main
dsh plugin --profile  add github:Nesarf/dsh-shell-surfaces#v1.0.0  # a tag

The tag is the version that can be named. Installing from main gets whatever it holds
today; installing a tag gets the commit that was decided on -- which is what "install the
same thing again tomorrow" and "go back to what worked" both need. v1.0.0 is the first
such point: thirty-five faces, two views of the same rows, and sizes a reader can drag.

The badge is the whole test workflow: the pure self-test, and a live check that
runs one real command on every surface the runner has -- on Windows and on Linux --
then feeds those answers into the same table a person reads. A green badge means
"the table and the argv shape still hold, and both platforms still answered", not
just "the linter passed".

One DSH tool for the execution surfaces this machine has -- thirty-five faces at
the last measurement: a WSL Linux userland and the ten shells inside it, the MSYS2
userland's six, four interpreters, both PowerShells, four window hosts, two container
runtimes, a remote destination, a real console, and nushell.

It exists because a rule that is only written down is not a capability. The
selection rules for these surfaces have been recorded for a while, but a session
could only act on them by composing a command string for pwsh and hoping the
quoting survived the trip. Here they are callable.

shell_surfaces(action="surfaces")                    what exists here, and where each was looked for
shell_surfaces(action="run", surface="wsl",  command="grep -P '(?<=Linux )[0-9.]+' /proc/version")
shell_surfaces(action="run", surface="nu",   command="open data.json | where n > 3 | to json")
shell_surfaces(action="run", surface="msys", command="pacman -Q | wc -l")
shell_surfaces(action="run", surface="pwsh", command="Get-CimInstance Win32_OperatingSystem | Select-Object Caption")
shell_surfaces(action="run", surface="pty",  command="powershell -File install.ps1", send=["y\n"])

Contents

The file is long because each section answers a question a person actually asked
while using this, and the answer is not one sentence. The map below is the short
version: read the section you need, skip the rest.

What you want to know Where
is PowerShell still the fallback? PowerShell is a surface here
can a command outlive the call? Background work stays here too
does confinement apply here? The same sandbox as the built-in shell tools
can state survive between calls? Persistent sessions
what else is installed on this machine? Finding this machine's shells
run Python, Perl, JS, VBScript Interpreters are faces too
reach another machine An ssh destination is a surface
run it in a container A container is a face too
what does the list look like? The list is a tree
which faces work together? Who works with whom
why one tool instead of many? Why one tool and not seven
what does a call render as? The row a call renders as
a surface refuses to answer Opening a path instead of refusing it
what is deliberately not supported? What it is not
why are the arguments shaped that way? The facts behind the argument lists
how do I run the checks? Verification
what is still unverified? Honest boundaries

PowerShell is a surface here

"Fall back to pwsh" used to mean "leave this tool and use the other one". The
pwsh surface closes that gap: a PowerShell command is an ordinary call here,
the same shape as every other surface, so a session that hits an awkward surface
does not have to change its tool to get a PowerShell job done. Background jobs,
timeout promotion, escalation approval and persistent sessions work on it as
well -- they are surface features here, not privileges of one tool.

Both PowerShells are addressable, and that took two entries rather than one. pwsh
is PowerShell 7 and is the better default; powershell is the 5.1 that ships
with Windows. Keeping them behind a single DSH_PWSH variable meant a machine
holding both could reach only one of them, while "does this script still work on
5.1" is a question two surfaces answer and one variable cannot.

The one shape this tool does not carry is a terminal session that keeps a drawn
prompt or a cursor. That is where the built-in pwsh tool still earns its place;
"the command is PowerShell" is not a reason to go there.

A surface-level failure is also not a reason to change tools. When a surface is
absent, cannot spawn, or times out, the tool says so and names the next call
(action="surfaces") that stays inside this plugin; the failure is about the
surface, not about the tool.

Background work stays here too

run_in_background: true starts the command as a job and returns its id: the
output ring streams it, job_output reads it, job_kill stops it. No timeout
applies. promote_on_timeout: true turns a timeout into that same job instead of
killing the process -- the output collected before the budget expired is handed
over exactly once, and the rest keeps streaming.

That is why the built-in pwsh tool now covers only approval/sandbox escalation
and the persistent session: the job machinery no longer sends a session back to
it. A composition without the jobs service gets a refusal sentence for a
background request rather than a job nobody can collect, and the pty surface
has no background mode at all, because a console transcript is read while the
program is alive.

The same sandbox as the built-in shell tools

A confined deployment confines this tool too. Each call resolves the session's
policy through ctx.sandboxPolicy, wraps the exact argv it is about to spawn
through ctx.sandbox.confine, and reports the mode, enforcement and denial facts
in the same vocabulary the built-in shell tools use. danger-full-access never
consults the provider, and a composition without the sandbox services runs
exactly as before.

A file effect the mode denies is a policy answer, not a command failure.
sandbox_permissions plus justification asks ctx.approval for exactly one
wider call -- the grant is consumed by that call and never persisted -- and an
escalation that is refused, cancelled or has no approval channel stops before
anything runs. The same-turn denial hint rides the tool result, so the sanctioned
retry does not depend on recalling the description.

Persistent sessions

Pass session: to run a command inside one long-lived shell per (agent,
surface, name): the current directory, variables and modules persist across
calls, and session_reset: true starts a fresh one. The session is a process
with a pipe, not a terminal -- a random nonce line marks where one command's
output ends, which is the only signal every supported shell prints without a
prompt.

Ten surfaces carry a session, in three shapes:

  • pwsh, msys, git-bash, zsh and tcsh keep exe + a flag list
    (-Command - / -l -s / -s), which is the default shape.
  • dash, brush and busybox have no login flag to give (dash is the strict
    POSIX sh by design, brush has no mount table, and BusyBox is ash), so their
    session is -s under the PATH the table sets -- busybox sh -s for the last.
  • wsl needs the distribution on its command line (wsl.exe -d Nyarch -- bash -l -s), so its entry builds the argv itself through argvFor. Measured
    2026-10-24: cd /tmp && export DSS_PROBE=41 then echo "$DSS_PROBE:$(pwd)"
    answered 41:/tmp -- the directory and the variable both survived.

fish needed a different shape rather than a refusal. It treats a piped stdin as
a script file and waits for EOF before running any of it -- measured
2026-10-24, with -l and with -l -i alike, a session that writes one command at
a time got silence. The opening is to keep stdin away from the script parser: the
session argv runs fish's own line reader, while read -l dss_line; eval $dss_line; end, so each line is evaluated on its own. Measured: cd /tmp and
set -x DSS_X 41 survived into the next call as X=41;P=/tmp.

A session is a real process, and three things close one: the sweep before each
session call (reapIdleSessions, thirty minutes of idle by default,
DSH_SESSION_IDLE_MS to change it), a timer that sweeps the same budget on its
own clock so a shell nobody returns to is still noticed, and the disposal of the
conversation that owned it -- session/disposed closes the shells of every agent
that named that session. Plugin unload closes whatever is left. A call that closed
something says so in its message, because the next call to that shell starting
fresh is otherwise a surprise.

A derived wsl- face deliberately has no session: the session's argv
is bash -l -s, so a session there would quietly run bash while the row claims
elvish or ysh. A session runs one command at a time, so the job flags are refused
with it, and pty has no session at all: a console transcript is read while the
program is alive.

Finding this machine's shells

The table is a set of defaults, and the scan is what makes them true on a
machine that moved its installs. The model is borrowed from
mega-index-map's library_detect: a built-in candidate table (shell names, the
usual install roots, the WSL launcher) resolved against PATH and this machine's
own locations, with every hit carrying where it was found. A scan costs one
PATH listing per directory, a few stat calls, one wsl -l -q, and one
command -v inside each distribution -- it is not a walk of the drive.

What the scan adds to the table:

Found Becomes
a WSL distribution other than the default its own wsl- surface
a Git-for-Windows or Cygwin bash a git-bash surface (the MSYS2 one keeps msys)
cmd.exe a cmd surface (cmd /c)
a terminal program that can be driven (wt, mintty, wezterm, tabby) a face of its own -- through the dss-exec shim for the first two, its own pane API for WezTerm, a private profile for Tabby (see "Opening a path instead of refusing it")
a terminal program with no such entry (alacritty, conhost, kitty without a GUI session) reported in discovered with an openWith line saying how it could be reached, if at all
a terminal installed inside a distribution (kitty, tmux, screen) reported with wsl: as its source; the Windows PATH cannot see it
tcsh or BusyBox in the MSYS2 userland surfaces of their own: tcsh (-c) and busybox (sh -c)
dash in the MSYS2 userland a dash surface: the strict POSIX sh, where a bash-ism fails instead of being forgiven
a shell installed inside a distribution (elvish, ksh, xonsh, mksh, tcsh, BusyBox) its own wsl- surface, which runs that shell rather than bash
oils-for-unix in a distribution two faces, wsl-osh and wsl-ysh: the bash-compatible half and the strict half that rejects what bash forgives

A shell that lives inside a distribution used to be reachable only by writing
bash -lc 'elvish -c ...' by hand -- a wrapper the caller had to quote correctly
every time, for a shell the scan had already found. wsl-elvish (and its
siblings) run the shell itself: the command is written to the distribution's
stdin as exec elvish -c '', so the outer bash gets one word it passes
through untouched. wsl-bash does not exist on purpose -- wsl already is bash.

The command travels on stdin, not as an argument

wsl.exe recomposes the words it was handed into one command line and hands that
line to the distribution, where it is parsed once more. A $VAR in an argument
was therefore expanded by a shell that had never run the loop defining it.
Measured 2026-10-20, through this surface:

for n in alpha beta; do echo "n=[$n]"; done   ->  n=[] and n=[]   (wanted alpha, beta)
echo 'single=[$HOME]'                         ->  single=[/home/...]  (single quotes lost too)

while the same commands on the msys surface answered correctly. Base64 behind
eval did not help -- that wrapper was re-parsed the same way. The one channel
wsl.exe does not rewrite is stdin, so the command now goes there and bash reads
it with -l -s. Scripts are unaffected: a file path was always safe.

The distribution probe is bounded: a cold instance that outlasts the budget
reports nothing for that distribution instead of blocking the scan.

action="surfaces" answers with both halves: the surfaces this tool can run
now, and the discovered report (kind, name, path, source) the scan produced.
A discovered path also replaces a guessed default, so a machine whose
PowerShell lives outside Program Files still answers.

refresh: true re-runs the scan on a surfaces query, so a shell installed since
start-up appears without a restart. The surface argument is an open string for
the same reason: the scan decides the ids, and the executor validates one against
the scan it is holding rather than against a list fixed at registration.

Interpreters are faces too

python, node and perl are surfaces of their own rather than something to
spell out inside a pwsh call. They pass the same test every shell here passes:
they take a command and answer with stdout and an exit code. Their kind is
interpreter, and the guide says what makes them different -- the code travels
after -c (Python), -e (Node), or -e (Perl), and a pipeline belongs inside
the code rather than around it.

Perl gets one environment entry of its own: PERL_BADLANG=0. Measured on this
machine, a Perl run under the UTF-8 request warns about a locale it cannot honour
before printing anything, and that warning arrives on stderr on every single call.
The variable silences exactly that warning and changes nothing else.

cscript is the one interpreter whose input is a file: cscript //nologo
takes a .vbs path and has no -c. That is what inlineFile exists for -- the
command is written to a temporary .vbs in the spill directory, cscript runs it,
and the file is removed the moment the run settles. Measured: the code never
appears in argv, and the spill directory does not grow across calls.

An ssh destination is a surface

The host list comes from the user's own ~/.ssh/config, and ssh is already the "flags then command" argv shape every shell surface
uses, so an ssh destination needs no new machinery: each host becomes an
ssh- face whose flags end with the host and whose command is the last
argument.

Two kinds of entry are dropped on purpose, because a face that fails on every
call is worse than no face:

  • a host with User git -- that speaks the git protocol, not a shell, and the far
    side refuses anything else. This machine's config carries github.com with
    User git, and it is dropped for exactly that reason.
  • a wildcard (Host *.internal) -- that is a pattern for matching, not a
    destination that can be dialled.

BatchMode=yes is not a preference. Without it a host that wants a password sits
at a prompt nobody can type into, and the call reads as hung; with it, that host
fails at once and says so.

StrictHostKeyChecking=accept-new is the other half of that argument. A host seen
for the first time is recorded; a host whose key has changed is still refused.
Without it the first call to any new destination dies with "Host key verification
failed", which reads as a broken face rather than as an unrecorded host -- measured
2026-10-24 by clearing the entry from known_hosts and calling the surface.

When ssh cannot get as far as the command it answers 255, and the result carries a
note saying so: an unreachable host, a refused key or a changed host key is a
surface-level problem, not a failed command. Measured 2026-10-24 by stopping the
daemon and calling the surface (exit=255, Connection refused, note present);
with the daemon up the same call answers and the note is absent.

Standing up a loopback destination, so the face can be tested at all

This machine's ssh family was empty -- its only config entry is git-only -- and a
face nobody can call is a claim, not a capability. Nothing here needs a second
machine. Measured on this one (2026-10-24) with the distribution's own openssh
10.3p1:

# inside the distribution, as root
ssh-keygen -A                         # this image had no host keys at all
mkdir -p /run/sshd
/usr/sbin/sshd -p 2222 -o ListenAddress=127.0.0.1 \
-o PubkeyAuthentication=yes -o PasswordAuthentication=no

then one dedicated keypair on the Windows side, its public half in the
distribution user's ~/.ssh/authorized_keys (700 / 600), and this in
~/.ssh/config:

Host dss-loopback
HostName 127.0.0.1
Port 2222
User nyarch
IdentityFile ~/.ssh/id_ed25519_loopback
IdentitiesOnly yes

action="surfaces" then reports ssh-dss-loopback [remote], and a command
through it answers in 367 ms. The daemon binds loopback only and accepts keys only.

It can outlive a distribution restart, which is worth doing: a face that disappears
on reboot fails the first time anyone needs it. The image runs systemd
(/etc/wsl.conf sets systemd=true) and sshd_config already includes
/etc/ssh/sshd_config.d/, so systemctl enable --now sshd is enough -- the
drop-in above supplies the port and the key-only policy. Measured 2026-10-24 by
wsl --shutdown and waking the distribution again: 15 s cold start, then
ssh-after-restart with up 0 minute, active, enabled.

A trap worth writing down: generating that key with -N '""' in PowerShell makes
the two quote characters the passphrase. The symptom is not "wrong passphrase" --
the server says Server accepts key and then the client never sends the signature
(we did not send a packet), because BatchMode cannot ask. ssh-keygen -y -P ''
is the check: it prints the public key when there is none.

rbash is scanned and reported but given no face: a restricted shell refuses the
first cd, PATH assignment or redirection, so a surface that looks like bash
and then declines to do bash things would mislead. Shells that are merely not
installed -- yash, posh, oksh -- are already in the candidate table;
install one and the next scan reports it, and promoting it to a face is a
two-line change.

A container is a face too

Every container runtime the scan finds inside a distribution gets one:
run --rm sh -c '', handed over on stdin the same way a
derived shell's command is. It is deliberately throwaway -- a face is one command,
not a session -- so nothing persists between calls; when state matters, wsl is
the surface to use. A machine with neither runtime reports neither face.

Both runtimes are present on this machine (2026-10-24) and they are not
interchangeable: podman is rootless, docker is not. DSH_PODMAN_IMAGE and
DSH_DOCKER_IMAGE (or one DSH_CONTAINER_IMAGE) pick the image, default
docker.io/library/alpine:latest, pulled on the first call.

Getting them to run took four unrelated fixes, none of them the runtimes' fault:

  • libphonenumber pinned an older protobuf than podman wanted. Upgrading the two
    together resolved it -- the repository's current libphonenumber already
    depends on the new protobuf.
  • the pacman keyring was a 32-byte empty pubring.kbx, and every install failed
    with keyring is not writable. pacman-key --init plus --populate archlinux
    rebuilt it with 180 keys. Note that pkill -f gpg-agent is the wrong way to
    quiet the agent: the pattern matches the shell running it, which kills the
    command itself. The keyring went bad again after wsl --shutdown, so the
    rebuild is a thing to expect rather than a one-off.
  • rootless podman needs libsubid.so.6, which the installed shadow (4.18.0) did
    not provide; that package was upgraded for it. With subuid/subgid already in
    place the container ran first try: container-face-ok and ID=alpine in 2.6 s.
  • docker could not pull: dockerd uses the distribution's own network stack and
    the registry resolved to an IPv6 address that does not answer here
    (dial tcp [2a03:2880:...]:443: i/o timeout). The image came over from podman
    instead -- podman save docker.io/library/alpine:latest | docker load -- and
    docker run --rm alpine:latest then answered docker-ok. Adding the daemon to
    the docker group is what lets the surface reach the socket at all.

A container that is already running is a face of its own

exec sh -c '', derived from docker ps / podman ps in
the same scan, so the face exists exactly while its container does. It needs a shell
inside the image -- a scratch image has none, and the call says so rather than
pretending.

The difference is measured, not asserted (2026-10-24, writing a file through both
doors and reading it on the next call):

face write read on the next call
docker-dss-docker -- exec into a running container kept kept
docker -- one-shot run --rm kept marker-gone

Getting the container list cost one lesson: the scan script goes on stdin, not
as an argument. With it as an argument the list came back empty while docker ps
answered fine by hand -- wsl.exe recomposes what it is handed and parses it again
inside the distribution, which is the same trap the wsl surface's own commands hit.

The list is a tree

action="surfaces" draws the table as a tree, because a userland that hosts
several shells is one place with children:

wsl [userland] available
· a whole Linux userland (WSL2, default distribution Nyarch): the real grep -P, ...
· when: Use when the job needs something only Linux has: ...
wsl-zsh [shell] available
· zsh inside the Nyarch distribution, as a surface of its own
· when: Use when the command is written for zsh itself: ...
wsl-dash [shell] available
· dash inside the Nyarch distribution, as a surface of its own
· when: ...
msys [userland] available
· the MSYS2 userland bash: a Unix userland on Windows, with pacman for its own packages
· when: ...
zsh [shell] available
· zsh inside MSYS2: the same userland as msys, a different shell language

The rule is the one a tree listing follows, and it is worth stating because a
flat list says nothing about who owns what:

  • a face is printed at its own depth, four spaces per level;
  • its children are printed next, one level in, each at that same depth;
  • when the last child is done, the next face starts back at its own depth --
    msys above begins at the left edge because it is a root, not because it is the
    next line;
  • the two lines explaining a face sit one level in and are led by a middle dot
    (· summary, · when:). A note carries no [kind], which is what tells the two
    apart at the same indentation.

The relation is derived (family / parent on every row) rather than written
into each entry, so a row added later cannot forget who it hangs under.

The text above is what a reader and a model see. The client draws a treemap
instead, and a listing is always open -- it is a map of the machine rather than a log
to peek at.

The layout is done by flexbox rather than by arithmetic in the plugin: one vertical
band per kind, its width a flex value proportional to how many faces the kind
holds, and one cell per face inside the band. Weights are equal because a face has no
size of its own -- counting faces is the only honest weight here. That choice is
deliberate: a coordinate layout computed once would be right for one window width
and wrong for every other, while a layout engine that already exists and already
handles resizing is the better tool.

Grouping is family first: a face that owns children becomes a band of its own,
holding itself and its children, so the map shows the same ownership the tree does.
A lone face falls back to its kind, which is the most useful thing left to say about
it. Family bands are drawn heavier than kind bands because they are a different
claim. An absent face is dimmed and struck through rather than hidden.

Three things the picture can do that the text cannot:

  • pick a cell. At 320 pixels for thirty-five cells the map can only show names,
    so a click opens a detail block under it with that face's summary and its full
    when:. A tooltip cannot stay open while a paragraph is read.
  • say what is not a face. The scan finds programs that never become rows --
    conhost.exe, tmux, screen, zellij, abduco, kitty, gnome-terminal on
    this machine. They get a dashed band of their own: putting them among the faces
    would make the map lie, and dropping them would withhold the answer a reader most
    often needs next. The text lists only the ones with no face at all, using
    openWith as the discriminator (face: ... means the face exists; shim or
    session host means it does not).
  • draw the crossings. Seven edges and eleven nodes is a size where a drawing
    beats a list, so the collaborations are a two-column graph: the borrowing face on
    the left, the borrowed one on the right, one curve per edge, the reason on the
    curve's tooltip. The a — b: how text form stays in the host output, which is what
    a model and a copied answer get.

A fourth thing the picture says, once there is data for it: how much each face is
used.
Every run increments a counter (in shell-surfaces-usage.json beside the
harness home, or at DSH_USAGE_FILE), the face line carries · 3 runs, and a cell
is sized 1 + log2(1 + runs). The compression is not decoration -- a face run ten
thousand times and one run ten should not differ a thousand-fold in area, or the map
stops being readable at either end. Equal cells were the honest choice while nothing
was known about use; they are a poor one once something is.

The count is taken after the call rather than before: a face that failed to spawn was
still reached, and a command that failed is still a run of its face. A request that
never got as far as a surface is not counted at all. A failure to write the file is
swallowed -- the count exists to make a picture better, and a picture is not worth
failing a command over.

Two views of the same rows

The table is the default. Six columns -- name, kind, status, runs, parent, summary
-- with a header row that stays put while the body scrolls and re-sorts the rows when
a column is clicked, zebra stripes, and numbers on the right in tabular figures.
Thirty-five rows of six attributes is a table's job: columns line up, so comparing two
faces is a glance instead of a re-read, and "which face runs most" is one click.
Density owns the row padding here too, so compact / normal / roomy apply to both views.

The map is the second view, one click away. It says the one thing a column cannot
say as quickly -- how much of the picture a face occupies -- and it is still the place
where a family band shows that a userland owns its shells. Both views read the same
parsed rows, so neither can drift from the text.

The box the picture sits in is the grip. Drag its corner and the treemap re-lays
itself out at the new size -- row height and column width together, because the
browser is doing the layout and nothing here computes a second one. It starts 380 px
tall, will not go below 320 x 220 (under that the drawing stops being readable) or
above 80 vh (over that it pushes the conversation off the screen), and scrolls while
the treemap keeps a 760 px floor and the crossing graph 460 px.

Every other call keeps the click, because its output can be a screenful nobody wants
by default.

Who works with whom

A tree can say who lives inside whom; it cannot say who works with whom. A
window face is not a child of node, and a container runtime is not a child of
wsl -- it drives a daemon that happens to run there. Those relations are recorded
as edges (cooperates on a row) and printed as their own section, one line per
edge:

· crossings: faces that work together rather than live inside each other
· wt — node: the answer comes back through scripts/dss-exec.mjs
· wezterm — cmd: each pane runs a cmd /k command line
· docker — wsl: the daemon runs inside the distribution; this face only drives it

Two reasons it is a section rather than a second tree. An edge can join any two
faces at any depth -- a window at the top and an interpreter at the top, a container
and a userland -- so a line drawn between two rows would have to depend on where
those rows happened to land, and the same table could then be read two ways.
And the tree's job is ownership: a line that reads like a parent link is the one
mistake this section exists to avoid. node is not inside wt; it is used by it.

The client renders the section as notes, one per row, so it stays visually
subordinate to the tree without pretending to be a child.

Why one tool and not seven

Every registered tool spends part of the context on its description. Thirty-five
tools would say the same thing thirty-five times. The selection rules -- which surface for a
structured pipe, which for a Linux-only tool, which for a program that waits for
input -- live in this one description, and a session reads them exactly when it
is choosing.

This tool is the first choice for running a command, not a fallback for
"exceptional" cases: a job that needs a Linux tool, a POSIX pipeline, another
shell language, a structured pipe, a real console -- or PowerShell itself --
belongs on its surface. One prompt section says that, placed ahead of the pwsh
rule, because a tool description only speaks once the catalogue is already open.
Wrapping another shell inside a pwsh command line stays the thing to avoid: the
extra quoting layer is where paths get mangled.

The row a call renders as

A shell_surfaces call used to render as the wire tool name --
"工具调用 · shell_surfaces · wsl" -- which names the catalogue entry rather than
the machine surface or the command. The plugin now ships a client half
(lib/client.js) that registers a tool.call.toolview row for this tool, so a
call reads as the sentence it is:

WSL · grep -P '(?<=Linux )[0-9.]+' /proc/version      完成 · 120ms
zsh · ls                                            1 · 12ms
conPTY · cmd /c ver                                  运行中
shells · 32 of 32 surfaces are on this machine       完成

The surface name is at the head, the activity beside it, the exit status at the
end, and the captured output one click away. That is a client concern rather
than a host result: the pending row must already name its surface before any
output exists. Nothing patches a first-party package -- tool.call.toolview is
the supported keyed seam, and the stock row remains the fallback for every other
tool.

Opening a path instead of refusing it

A host that cannot answer is a problem with four shapes, and three of them have a
physical fix -- a piece that runs beside the host rather than an argument that
hopes the host cooperates.

Shape Opening Measured
a window that keeps its output (Windows Terminal, mintty) the shim: the window runs node scripts/dss-exec.mjs, the shim runs the command and writes {exitCode, stdout, stderr} to a file, and this tool polls that file wt answered wt-ok in 53 ms and carried cmd /c exit 5 back as exit 5; mintty answered in 39 ms; both are faces now
a program with its own control channel (WezTerm) the pane: wezterm cli spawn gives a pane a command, get-text reads the screen back wezterm-ok in 225 ms, ver in 227 ms
a program that refuses everything else on purpose (git-shell) the channel: it accepts only git transport, so a pre-receive hook in a bare repository runs on push and its output rides back on the push report git-shell -c 'echo x' answers fatal: unrecognized command -- the refusal is the design, so the opening is another protocol, not a wrapper
a multiplexer (tmux, zellij, abduco) the session host: driven headless (new-session -d + capture-pane) rather than as a window capture-pane returned tmux-ok and rc=0 from a detached session

Two more openings, both measured 2026-10-24:

  • rbash refuses cd, PATH changes and redirection (rbash -c 'cd /tmp' answers
    cd: restricted), and a plain bash -c from a non-restricted shell restores all
    three -- the opening is a second shell, not a flag.
  • mshta starts the shim like any other launcher: an HTA page whose
    WScript.Shell.Run(..., 0, True) runs dss-exec.mjs with the child window hidden.
    Measured: the report came back with mshta-ok in 316 ms and mshta itself exited in
    1.3 s -- it does not hang the caller the way wt --help did. (node still has to
    be on PATH; an HTA has no shell to inherit one from.)

Tabby needed a different opening. It takes Electron switches only -- no -e,
no new-tab -- -- but it does have tabby profile, and a local
profile carries options.command + options.args. Those are static, so the
profile names a fixed agent (scripts/dss-tabby-agent.mjs) once and every call
only rewrites the job file that agent reads. scripts/tabby-setup.mjs writes that
profile into a private instance's own config (--user-data-dir), never into
the user's: handleOpenProfile calls bringToFront() on every open, so driving
someone's Tabby would yank their window forward on every call.

Two measured traps shaped it:

  • a bare command: node does not work -- Tabby starts the profile and the agent
    never runs, so the profile names the interpreter by absolute path;
  • cmd /c start /min is not a way to keep the window out of the way: start
    takes the quoted program path as its window TITLE and then tries to launch the
    next word as a file, which produced a "Windows cannot find 'profile'" dialog and
    hung the caller. The window is minimised after the fact instead, and after the
    answer arrives
    , because bringToFront() un-minimises it while the profile
    opens.

Tabby.exe profile dss itself never returns (it blocks for the application's
lifetime), so the launch is detached and the result file is the only thing waited
on. Measured: the agent answers in about 45 ms, and the whole call -- including
minimising the window -- finishes in about 4 s once the instance is warm.

action="surfaces" carries this per program: every discovered terminal comes
back with an openWith line, so "reported but not a face" is a next step rather
than a shrug.

What it is not

  • No message hooks. It registers a tool, a prompt section and a client row;
    the message stream is not touched, so it cannot be the plugin that writes a
    malformed message.
  • No terminal emulator. A terminal emulator draws text for a person and adds
    nothing a subprocess can use. The one surface that needs a console talks to
    ConPTY directly.
  • No window launcher that cannot answer. Windows Terminal and mintty are
    driven through the dss-exec shim, WezTerm through its own pane API, and Tabby
    through a private profile; each returns a structured result rather than a
    window's contents. A terminal program with no such entry stays a scan report,
    and the report says which entry it would need: wt --help measured as a window
    that never exits, and kitty needs a GUI session this machine cannot give it.
    git-shell was refused for its own reason: it rejects every command that is not
    a git upload/download (fatal: unrecognized command). A face that cannot answer
    is worse than a report that says so.

The facts behind the argument lists

Each of these was measured on this machine, and each shapes the argv:

Fact Consequence
A command handed through a shell can be re-parsed twice The command travels as one argv element; a script file is preferred over an inline string
wsl.exe recomposes its arguments into one command line, which is parsed again inside the distribution the command travels on stdin (bash -l -s); as an argument it lost $VAR values to a shell that had not defined them, and even single-quoted literals lost their value
MSYS2's /tmp follows TMPDIR, which this machine passes down as a Windows path msys / zsh / fish run as login shells, which normalises the environment
brush has no Cygwin runtime beside it, so /tmp is :\tmp brush gets no login flag and no POSIX-path assumptions; its PATH needs usr\bin as well as ucrt64\bin, or every external command fails while the shell reports a healthy version
PtyProcess.read blocks Reading happens on its own thread; a read loop that also decides when to stop can block the very check that would stop it
A console wants Enter, which is a carriage return send normalises a trailing newline (real, escaped, or literal) to CR, and a bare answer gets one added
wsl.exe sometimes prints the result and then declines to exit A timeout with output is reported differently from a timeout without it; wsl.exe --shutdown is the reset (measured: a cold instance answered in 58 s, the same call after a reset in 8 s, and warm in 0.13 s)
PowerShell parses what follows -Command as a command line, so a script path handed that way is a command, not a file a script gets -File, a command line gets -Command; both always run -NoProfile -NonInteractive
pwsh 7 and Windows PowerShell 5.1 both answer UTF-8 through a pipe (measured 2026-10-15) the runner's single UTF-8 decode needs no per-surface exception
The job registry accepts each chunk through job.append, not through a read source the producer pushes bytes as they arrive, and a promoted process flushes what it already produced exactly once
SandboxProvider.confine wraps argv, not a command line the surface's exact argv (program, flags, command) is what gets wrapped, so nothing re-parses it
An approval grants one call, not a session a wider mode is consumed by the call that asked; the next call starts from the standing policy
A shell reading a pipe prints no prompt a random nonce line ends each session command's output; the command's own text can never be mistaken for the marker
A scan that walks the drive is the thing it must never become the candidate table is the whole search space: PATH, the usual install roots, and one wsl -l -q

Layout

Path What it is
lib/index.js the tool: one shell_surfaces (run, surfaces, background, promote, sandbox escalation, sessions) and the selection-rule prompt section
lib/sessions.js the persistent session pool: one marker-delimited command at a time, per (owner, surface, name)
lib/client.js the client half: the tool.call.toolview row that renders as ·, in the module system's own bundle format
lib/surfaces.js the table: paths, arguments, environments, and the absent-reason for each surface
lib/discover.js the scan: shell names, install roots and the WSL listing, each hit with its source
lib/runner.js argv construction, bounded output with spill, timeout classification
lib/usage.js how often each face has run: one counter per id, written through a rename, and a write that fails is swallowed
scripts/pty_runner.py the console: pywinpty, a reader thread, ANSI stripping, one JSON report
scripts/live-check.mjs runs one real command per surface through the plugin's own code path
scripts/probe-faces.mjs reads the surface TABLE and asks every present face what it is; a face added to the table is covered without being added here too
scripts/probe-all-shells.ps1 a version census for the terminals and the long-standing shells, from a hand-written list; platform-shaped, one JSON object out
scripts/probe-posix.sh, scripts/probe-wsl.sh the POSIX and WSL probes, as files, because an inline command crosses quoting layers and a $() gets expanded by the wrong one
scripts/probe-table.mjs the probe's JSON as a markdown table, also written to $GITHUB_STEP_SUMMARY
scripts/pty-gate.ps1 the interactive fixture: a Read-Host prompt for the pty case
scripts/pty-gate.sh the same fixture for a POSIX console: read needs a terminal, which is the whole point of a pty
scripts/minimize-window.ps1 does the one thing no terminal program offers a flag for: minimises the window a surface opened, matched by PID or by a per-call marker in its command line
scripts/which-surfaces.mjs one line per surface, present or absent with the path looked for -- readable on its own, and CI's first step when the table cannot even load
test/surfaces-test.mjs the table, the argv shape, the tool contract, the prompt section
test/client-bundle-test.mjs the bundle format, the keyed row registration, and the sentence the row says in every call phase

Verification

node test/surfaces-test.mjs          # no host needed; the host validator is asked too when reachable
node test/client-bundle-test.mjs     # the real client bundle behind a fake module loader
node scripts/live-check.mjs          # one real command per surface on this machine
node scripts/live-check.mjs wsl nu   # or only some of them
DSH_LIVE_TERMINALS=1 node scripts/live-check.mjs wt mintty wezterm tabby   # the window faces, which open windows
node scripts/probe-faces.mjs | node scripts/probe-table.mjs   # every surface in the table
pwsh -File scripts/probe-all-shells.ps1 | node scripts/probe-table.mjs   # terminals + the long-standing shells

live-check.mjs is the probe now. With LIVE_CHECK_JSON=1 it writes the JSON
probe-table.mjs eats to stdout and its human-readable lines to stderr, so one run
answers both "is this surface alive" and "what version is it". CI uses that form;
the plain form is what a person runs.

probe-faces.mjs came first and does the census without asserting anything: it
reads the table and asks every present face what it is. It stays for a quick "what
is on this machine right now" that cannot fail a build.

probe-all-shells.ps1 stays for what the table cannot answer: the terminal
programs without a face
(alacritty, conhost, kitty here) still have no entry, and
a version census is where their presence is recorded.

The live check covers the interpreters and the shells inside a distribution as
well, and it opts out of the window faces by default: a routine run should
not put four windows on someone's screen, so wt, mintty, wezterm and tabby
are exercised only when DSH_LIVE_TERMINALS=1 or when named on the command line.
Measured on this machine: the default set is twenty-six surfaces (twenty-five
answering, one skipped because its nushell cannot run on that distribution's
glibc), and the four window faces answer wt-ok / mintty-ok / wezterm-ok /
tabby-ok in 0.34 to 1.05 s once their instance is warm (tabby's first call pays
for a cold start).

CI runs the self-test, and runs the probe on both a Windows and a Linux runner
(the tables land in the run summary, so "which surfaces exist" is a record with
history rather than something a person measured once). Each runner also runs this
live check against every surface it actually has.

That last step reports four outcomes, not two: pass, fail, absent and
skipped. A surface a machine does not have is a fact about the machine
(absent, with the path that was looked for); a surface that exists but cannot be
exercised here -- a WSL launcher with no distribution, a python without pywinpty --
is also a fact about the machine (skipped, with the reason). A surface that ran
and answered wrongly is a fact about the plugin, and only that fails the job,
because a runner with nothing to run is not a broken build.

A pass also needs the expected output, not just exit code 0: bash -c "" exits
0, and the failures this check exists to catch (a command mangled by quoting,
output decoded as the wrong encoding) are exactly the ones that keep exit code 0.

Its table goes into the run summary beside the probe's, so "this runner is missing
half the surfaces, and here is why" is answerable without opening the log.

The first full pass (2026-10-15) ran every surface the table had then: wsl returned
the kernel and a /proc count, nu returned a number through a structured pipeline,
msys returned its bash version and a package count, zsh its parameter-expansion
result, fish its arithmetic result, brush an uppercased string, and pty answered a
Read-Host prompt with y and printed the branch it took. That list has since
grown to thirty-five faces -- which is exactly why the live check reads the table
instead of keeping a list of its own.

Honest boundaries

  • Windows-first. The surfaces it knows about are this class of machine's: a
    WSL distro, MSYS2, nushell, and ConPTY. On POSIX the platform already ships a
    single shell, so most of this is unnecessary there.
  • No isolation of its own: a command runs with the harness process's rights.
    The file sandbox and approval policy still apply to the process tree.
  • MSYS-family output is assumed UTF-8 (LANG=C.UTF-8 is set). A Windows program
    reached through a surface may still answer in the OEM code page.
  • The MSYS2 surfaces need DSH_MSYS_ROOT if MSYS2 is not at D:\msys64;
    nushell needs DSH_NU if nu.exe is not in %LOCALAPPDATA%\Programs\nu\bin;
    the pty surface needs python on PATH with pywinpty installed.
  • The pwsh surface is PowerShell 7 (pwsh.exe) by default; a machine with
    only Windows PowerShell sets DSH_PWSH=powershell.exe.
  • The pwsh surface can run in the foreground, as a background job
    (run_in_background), or promoted past its timeout (promote_on_timeout), and
    it carries escalation approval and a persistent session as well. The built-in
    pwsh tool keeps exactly one shape this one does not: a terminal session that
    keeps a drawn prompt or a cursor.
  • Background work and promotion need the jobs service. A composition without
    it gets a refusal sentence instead of a job nobody can read, and the pty
    surface has no background mode at all: a console transcript has to be read
    while the program is alive.
  • The sandbox seam is optional: without ctx.sandbox and ctx.sandboxPolicy
    nothing is confined, exactly as before. When it is mounted, confinement covers
    every surface, including wsl and msys; a surface the runner cannot launch
    under confinement reports the sandbox problem rather than running unconfined.
  • Sessions exist for ten surfaces -- the shells that can read commands from stdin,
    which is every shell here except pty and the four window hosts. A derived
    wsl- face has none on purpose (a session there would run bash while the
    row claims elvish), and a surface without one says so and names the ones that
    have it. A session is process-local: it does not survive a harness restart.
  • The scan runs once per process; refresh: true on a surfaces query re-runs it,
    so a shell installed later appears without a restart.
  • Of the terminal programs this machine has, four are faces (wt, mintty,
    wezterm, tabby) and the rest stay reports. pty remains the one real
    console, and the one shape no window face carries.

常见问题

6 问
答案由本站依据条目字段整理,不引用上游原文
dsh-shell-surfaces 怎么安装?

在 DSH 里执行 `dsh plugin --profile web add github:Nesarf/dsh-shell-surfaces#v1.0.0` 即可。

dsh-shell-surfaces 的验证等级是什么意思?

L4 · 沙盒安装通过。安装命令在隔离沙盒内真实执行成功,无依赖错误

dsh-shell-surfaces 安全吗?

低风险。

dsh-shell-surfaces 装之前要先确认什么?

先看它会读写哪些文件、执行哪些命令、连接哪些网络地址;验证等级只说明装得上、跑得起来,不等于安全。涉及密钥或系统权限的,建议先在隔离环境试装。

dsh-shell-surfaces 用的是什么许可证?

上游标注为 MIT。商用或二次分发前请到仓库确认许可证原文。

这条记录的数据可靠吗?

本站按仓库名去重后合并整理,字段以作者仓库与 npm 元数据为准;安装命令、仓库地址与 README 保持原文,带 L4 / L5 标记的安装命令已在隔离沙盒里真实跑通。

会话与消息
按星标排序,可作替代方案

dsh-desktop

dsh-desktop

为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」,桌面本身也是「插件」。

L3 · 安装规范通过 3.0 万 星

openpets

openpets

L4 · 沙盒安装通过 1,274 星

deeptide

deeptide

L4 · 沙盒安装通过 1,104 星

jingyun-dsh

jingyun-dsh

井云 DSH 客户端插件,为 DeepSeek Harness 注入会员登录、订阅支付、算力管控与应用市场,让智能体和技能快速变成可交易商品

L3 · 安装规范通过 1,004 星

study-mate

study-mate

StudyMate 是 DSH 的「学习模式」预设与技能包,为数学、计算机科目定制学习路线、讲解课件与练习评估,跨科目共享记忆。

未标注验证等级 856 星

Nesarf 的其它插件

2 个

插件的元信息来自作者仓库;分类、验证等级与安全结论的中文说明为中文整理,最近核验 —。