Zum Hauptinhalt springen
B

dsh-supervision

btspoony/dsh-supervision

Verbindet eine laufende DSH-Sitzung mit einem beliebigen Terminal-Supervisor, der ihren Status sehen und den Ablauf steuern kann.

Installation

dsh plugin --profile web add github:btspoony/dsh-supervision

README

preview

dsh-supervision

English | 中文

npm license node DSH

Bridge a running DSH session into any terminal supervisor — it sees the state, and it can steer. The plugin mounts inside the dsh web process you already run: OSC 7501 records go out on the terminal, and operator input comes back into that same session. herdr-core is one supported supervisor; any supervisor that reads OSC 7501 and can write lines back can drive a bridged session, with no changes to DSH or the supervisor.

What you get

  • One status record per session. The bridged session is the root record — idle, working, done, blocked or error; other sessions in the same process are child records carrying id=.
  • Operator input goes back in. Bare text steers a running turn, /cancel ends it, /status, /new and /attach inspect and switch sessions, and /approve or /reject settle parked asks — all from the terminal.
  • Approvals stop living in another surface. A parked ask turns the root record into blocked · kind=permission, is settled from the terminal, and the GUI shows a supervision banner with Take back control — so the person in the browser always sees who holds the session and has a way to take it back.

Works with

SupervisorHow it connectsWhat it needs
herdr-core (hc)in-process tty via its own HC_TERMINAL_ID (or DSH_SUP_TERMINAL_ID when the host exports one) and the control socket with dsh-sup in its PTY; both are open at oncea dsh web started inside the supervised terminal, plus any PTY for the forwarder
Any supervisor that reads OSC 7501control socket + dsh-sup in its own PTYit must read the PTY's OSC output and be able to write lines to that PTY's stdin
Another supervisor wanting in-process modeset DSH_SUP_TERMINAL_IDthe variable is read, never written, by this package; any supervisor can set it
GUI-only dsh webnothingthe plugin exposes an inert socket and nothing else

Quick start

Install

Plugins page — in DeepSeek Harness open the Plugins page, click Add plugin, enter dsh-supervision and install; then restart as the host asks. The GUI writes into the web profile, the same profile the command below targets.

CLI — for scripted setups:

dsh plugin --profile web add dsh-supervision   # the plugin, into the web profile
npm i -g dsh-sup                               # the forwarder, for the supervisor's PTY

dsh-sup is a separate package because it runs as its own program in the supervisor's PTY. Pin a version with @<version>.

Connect a session

dsh-sup                                  # create a session in the dsh web process's directory
dsh-sup --cwd ~/work/project             # create it elsewhere (resolved here, then sent absolute)
dsh-sup attach session-abc123            # attach an existing session (or: dsh-sup session-abc123)
dsh-sup --pid 4711 new                   # pick the backend when several dsh web processes run
dsh-sup list                             # list every session on every live backend

list prints one table for every live backend and binds nothing. PID names the dsh web process a row came from, so a multi-backend listing stays attributable:

SESSION            PID    CWD                LIVE  BRIDGED
session-4f2a…      4711   /Users/me/work     yes   -

Backends are found through $DSH_HOME/supervision (~/.dsh/supervision when DSH_HOME is unset). A bridge needs exactly one live backend; with several, the refusal names each candidate's pid and socket, and --pid picks one. --cwd applies to new and list only; list without it shows every session, with the CWD column naming where each one lives.

Answer an approval

Ask where the session stands, then answer the parked ask by id:

/status
session=session-4f2a… state=blocked turn=3 asks=3f9ac1

/approve 3f9ac1
[ask 3f9ac1] bash → allowed-once (source=supervisor)

/y and /n answer the only parked ask; with more than one parked they refuse and ask you to name it.

Take back control

While a session is bridged the GUI shows a supervision banner above the composer — Supervised from <terminal> (pid <n>) · approvals are answered there — with a Take back control button. It posts to the reclaim route; the bridge is dropped, the supervisor reports the clear record, and a dsh-sup forwarder exits 0.

How it reports

One dsh web process owns one session tree; the plugin mounts in it and opens its control socket, plus an in-process tty when the process runs inside a supervised terminal.

dsh web (the process you already run)
└── dsh-supervision plugin
      ├── control socket   $DSH_HOME/supervision/<pid>-<8hex>.sock   (0600)
      │     └── dsh-sup in the supervisor's PTY ── OSC 7501 + echo lines ──▶ supervisor (reads OSC 7501)
      └── in-process tty   (added when DSH_SUP_TERMINAL_ID or HC_TERMINAL_ID is set)
            └── OSC 7501 written to this process's own /dev/tty

Activation. The plugin always opens its socket discovery surface — the listener, the 0600 socket and the <pid>-<8hex>.json registry entry that names it — so a forwarder has something to connect to. The effect layer (session listeners, OSC reports, echo lines, stdin handling) mounts per bridge, which is the same fact as "a forwarder is connected". A GUI-only dsh web therefore exposes an inert socket and nothing else. With DSH_SUP_TERMINAL_ID non-empty (or herdr-core's HC_TERMINAL_ID) the process also supervises the terminal it runs in: the same records go to its own /dev/tty, while the socket stays open for a dsh-sup elsewhere. A session still has exactly one bridge across both surfaces, so a second attach is refused as busy. If the terminal cannot be opened — no /dev/tty and a non-TTY stdout, or a stdin that is not a terminal — the plugin writes the reason to stderr and keeps the socket surface.

For implementers & agents

This half is reference material for implementers and coding agents; if you only want to use the plugin, jump to Links.

Repo map

PathWhat lives there
src/the host plugin and the browser supervision banner (src/client/)
packages/protocolthe OSC 7501 encoder and the NDJSON frame codec
packages/clithe dsh-sup forwarder
lib/build output
docs/figures and their captions
.agents/adr and .agents/knowledgedecisions and the architecture notes behind them
.changes/release fragments
locale/plugin-page strings

Supervision protocol

The facts a supervisor needs, all of them on this repository's wire:

Socket and registry. One Unix socket per dsh web process, mode 0600, at $DSH_HOME/supervision/<pid>-<8hex>.sock, next to a <pid>-<8hex>.json entry {proto, pid, socket, startedAt, dshVersion} written atomically and mode 0600. A session has exactly one bridge: a second connection claiming it is refused with error code busy and closed. Frames are NDJSON, PROTO is 1, one frame is at most 64 KiB; the forwarder splits its own stdin on \r, \n or \r\n and sends one line frame per input line.

DirectionFrames
forwarder → pluginhello (attach a session, new {cwd}, or bare), line {text}, list {cwd?}, bye
plugin → forwarderwelcome, osc {data}, echo {text}, sessions {items}, error {code, message}, bye {reason}

error.code is one of busy, unknown-session, create-failed, locked, protocol, internal.

OSC 7501 output. One record is replaced whole on every report, so every field is restated:

ESC ] 7501 ; state=<idle|working|done|blocked|error>[ :id=<child id>][ :kind=<permission|question|auth>][ :app=<word>][ :title=<base64>][ :msg=<base64>] ST

A clear is state=clear for the root, state=clear:id=<id> for one child record. kind is legal only on blocked; a parked approval is the root blocked:kind=permission record carrying msg=<tool> <reason> [ask <id>]. Caps: sequence under 4096 bytes, title 192 bytes, msg 2048 bytes, id 128 bytes as /-joined segments of at most 32 bytes each (8 segments). The root word is blocked > working > done / error / idle, and a parked approval outranks a live turn. The reporting surface is /dev/tty on both paths (stdout when it is a TTY); that is what the supervisor reads, not the plugin's or the forwarder's stdout.

Routes, served only when the host mounts a webserver:

RouteAnswer
GET /dsh-supervision/state{"proto":1,"sessions":{"<session id>":{"bridged":true,"terminal":"…","pid":<n>,"since":<ms>,"asks":["<ask id>"]}}}; 405 for another method
POST /dsh-supervision/reclaimcontent-type: application/json, body {"session":"<id>"} under 4 KiB → 200 {"reclaimed":true}; 409 {"reclaimed":false} when no bridge held it; 415 for another content type, 400 for a bad body

Echo lines the supervisor reads, in the order they are written: [turn <n>] <state> on a state change, [ask <id>] <tool> parked (<session>), [ask <id>] <tool> → <outcome> (source=supervisor|attached), the /status answer above, refused: <n> approval(s) parked, new failed: …, attach failed: …, unknown command /<verb>.

Exit codes of dsh-sup: 0 clean bye, 1 backend lost, 2 usage or no live backend, 3 busy, 4 locked, 5 create-failed, 6 protocol (a frame this process refused, so the backend is alive).

Configuration

Six keys, all of them read by src/index.ts; an unknown shape is refused with a field-level issue instead of half-applying.

KeyDefaultWhat it does
enabledtrueMount anything at all. false is a hard off switch: no socket, no routes, no banner.
programStatustrueEmit OSC 7501 reports. Off leaves the socket answering list only.
echotrueMirror state changes and approval decisions as echo lines for the supervisor.
app"dsh"The program word every report restates.
titleFrom"cwd-basename"Where the root title comes from: cwd-basename, session-title, or none.
socketDir$DSH_HOME/supervisionRegistry and socket directory.

Environment variables it reads:

VariableRead byEffect
DSH_HOMEplugin and dsh-supRegistry directory base; defaults to ~/.dsh.
DSH_SUP_TERMINAL_IDpluginNon-empty ⇒ an additional in-process tty transport; the control socket and registry stay open.
DSH_SUP_TERMINAL_NAMEpluginThe supervisor label the state route reports; defaults to dsh-sup.
HC_TERMINAL_IDpluginherdr-core compatibility: the same transport, read only when DSH_SUP_TERMINAL_ID is unset or empty.
HC_TERMINAL_NAMEpluginherdr-core compatibility: the label, read only when DSH_SUP_TERMINAL_NAME is unset or empty.

Both DSH_SUP_* variables and their HC_* fallbacks are read, never written, by this package.

Invariants

A change must not break these:

  • One bridge per session. A second connection claiming a bridged session is refused with busy; the in-process path holds one binding per process, and /attach switches it rather than adding a second.
  • A record is replaced whole. Every report restates every field that still applies; a field left out is a field cleared.
  • A parked approval outranks a live turn. The root word order is blocked > working > done / error / idle, and a session switch (/new, /attach) is refused while an ask is parked.
  • Teardown settles every parked ask before the socket closes. Disconnect, dispose, reclaim or an asker's withdrawal resolves each ask as cancelled; fail closed, never leave one dangling.
  • The terminal variables are read only. The in-process transport and its label are chosen by the environment, never by configuration, and nothing writes those variables.
  • Caps are hard limits. The encoder drops a record it cannot express within its caps rather than truncating it, and a frame over 64 KiB is refused locally.
DocContent
CHANGELOG.mdreleased changes

License

Released under the MIT License — the license field in package.json is authoritative.

Ähnliche Plugins