Skip to content

Latest commit

 

History

History
225 lines (174 loc) · 11.2 KB

CONTRIBUTING.md

File metadata and controls

225 lines (174 loc) · 11.2 KB

Contributing to dsh-remote-ssh

dsh-remote-ssh is a community plugin. It is not part of the DeepSeek Harness repository, it does not use DeepSeek's npm scope, and it is not affiliated with or endorsed by DeepSeek. The upstream project states that it cannot accept external pull requests at the moment, so nothing here is destined for it: this repository is the project.

Two guidelines from upstream shape how it presents itself, and both are respected here: descriptive text may say the plugin is built on DeepSeek Harness, and project naming uses the short DSH form.

Thanks for wanting to help. This plugin reaches into a running agent's filesystem and shell, so the bar is not "it works on my machine" — it is "it fails visibly everywhere else". The guide below is mostly about that.


1. Set up

git clone https://github.com/cmukanisa/dsh-remote-ssh
cd dsh-remote-ssh
npm run deps      # symlink the harness packages (see below)
npm test          # parser check + unit suite

Why npm run deps

The plugin imports @deepseek-ai/cordis, @deepseek-ai/dsh-fs, … exactly as the harness does, but this repository is not a harness checkout, so Node has nothing to resolve. scripts/link-deps.mjs symlinks those packages out of an installed harness instead of vendoring a second copy that would let the tests drift from the runtime they claim to test.

It looks in $DSH_HOME/profiles/node_modules/@deepseek-ai first, then at a global @deepseek-ai/dsh install. If neither exists:

npm install -g @deepseek-ai/dsh
dsh --profile web --help    # creates $DSH_HOME/profiles
npm run deps

npm install (used only for the template check's one dev dependency) prunes those symlinks, so re-run npm run deps after it.

There is no build step. The plugin is plain ES modules and the browser half is a hand-written bundle, on purpose: a build step is one more thing that can differ between a contributor's machine and a user's.

2. The test suites

Suite Command Needs What it proves
Parser npm run check nothing every shipped file parses; the browser bundles keep the module-table wrapper
Template npm run test:template npm install the composition template parses and replaces every shipped provider it must
Docs npm run check:docs nothing the three language pages exist, cross-link, have no dead internal link, and pull no third-party resource
Unit npm run test:unit nothing quoting, path mapping, mirrors, policy containment, the Remote descriptor, the composition template
Installer node test/install.test.mjs nothing the three durable changes, exactly once, and that the plugin arrives switched off
Windows client node test/windows-client.test.mjs nothing no multiplexing promise on Windows; ControlPath shortening holds for Windows-shaped paths
Tailscale node test/tailscale.test.mjs nothing tailnet parsing, address classification, peer lookup, preflight, and the two transports as argv. Synthetic fixtures only: a test file is public, so it must never carry anyone's tailnet
Filesystem E2E npm run test:e2e a real SSH host read/write/edit/list/stat/byte windows/URI parsing/sandbox fences, the shell executor, subprocess routing, and local passthrough
Detached work node test/sessions.test.mjs a real SSH host a command keeps running after the client that started it is gone, a new reader sees its state, and it can be stopped
Harness E2E npm run test:harness a running dsh + a real SSH host the composed plugin: browser bundle served, sshWorkspace Remote namespace answering, a remote folder becoming a workspace
Windows server node test/windows-server.expect.mjs a Windows SSH host the refusal is explicit and actionable

Running the filesystem suite locally

npm run ssh:up                 # builds and starts a disposable OpenSSH container
export DSH_SSH_TEST_HOST=127.0.0.1 DSH_SSH_TEST_PORT=2223 DSH_SSH_TEST_USER=dsh \
       DSH_SSH_TEST_KEY=/tmp/dsh-remote-ssh-e2e/id_test \
       DSH_SSH_TEST_KNOWN_HOSTS=/tmp/dsh-remote-ssh-e2e/known_hosts \
       DSH_SSH_TEST_ROOT=/home/dsh/work
npm run test:e2e
npm run ssh:down

ssh:up prints the exact environment for your shell. Pick a port nothing else listens on — a stray local sshd on the same port is the single most confusing failure mode there is.

The suite creates its own directory per run and never depends on state a previous run left behind, so it is re-runnable.

3. The CI matrix

.github/workflows/ci.yml — parser + unit + installer on Linux, macOS, and Windows across Node 22 and 24 — the range the harness supports, so a green run means something about a real deployment. This is the baseline; a red here is a genuine portability regression.

Two portability traps this matrix has already caught, both worth remembering: a Windows checkout with core.autocrlf hands over CRLF (so any line-anchored check must normalise first), and ssh -V prints on stderr.

.github/workflows/e2e.yml — four jobs, because a remote workspace is only "portable" if it survives all of these:

Job Server Client Why it exists
linux-server Alpine and Debian containers Linux the two Linux userlands the remote scripts may assume
harness Alpine container Linux the full product: boot, browser bundle, Remote namespace, workspace adoption
macos-server the runner's own sshd macOS BSD userland (stat -f) and the long /var/folders temp path
windows-server the runner's own sshd Windows asserts the documented refusal, and covers the Windows client path

.github/workflows/portability.yml — POSIX-shell conformance of install.sh, YAML validity of the composition template, a scan for machine-specific absolute paths, and the Windows transport expectations.

.github/workflows/release.yml — on a tag: build the tarball, prove it installs into a pristine harness home, publish it with a checksum.

.github/workflows/codeql.yml — security-and-quality analysis. The interesting class of bug here is untrusted data (a path, a command fragment) reaching a remote shell, so a review should look for exactly that.

4. Changing the code

Layout

packages/dsh-remote-ssh/        host: ssh.js tailscale.js registry.js fs.js shell.js subprocess.js sessions.js
packages/dsh-remote-ssh-ui/     browser half (localized) + the Remote namespace
patch/remote-ssh.patch.yml.tpl  the composition rows the installer writes
install.mjs, install.sh         installation
docs/                           the GitHub Pages site, fr/en/zh, no build step
docs/assets/social/             the link-preview and announcement cards
scripts/                        the static gates CI runs, plus social-cards.mjs
test/                           the suites above

The cards under docs/assets/social/ are generated, not hand-drawn: node scripts/social-cards.mjs renders them from the wording in that script with a headless Chrome (--theme light|dark, --out, --scale). Change the copy there and re-run it rather than editing a PNG.

Client copy is locale-owned: every displayed string is a key in the fr/en/zh dictionaries registered with ctx.locale, and scripts/check-syntax.mjs fails if a key is missing from a locale, unused, or if a literal reaches the DOM.

Rules that are not negotiable

  • Every value that reaches a remote shell goes through shellQuote. ssh host cmd a b joins its arguments with spaces and the remote login shell parses that one string, so an argument is not a separate word. A path or a command fragment interpolated raw is remote code execution.
  • Parse stat permission bits base 8. They are octal digits; reading 644 base 10 gives 420, and every later chmod corrupts the file it meant to preserve. This shipped once.
  • Never let a local path take the remote branch, and never the reverse. registry.worldOf() is the single decision point; the local passthrough tests exist because a regression there breaks every ordinary session.
  • Fail visibly. A misconfigured host must not read as an agent bug: name what is wrong and what to do about it (see the non-POSIX refusal).
  • Keep side effects reversible. Anything that opens a connection, a timer, a slot, or a settings namespace belongs to the plugin's fiber.

Adding a remote operation

  1. Put the remote shell source in the layer that owns it (fs.js, shell.js, …) and quote every interpolation.
  2. Map its failures into the seam's own taxonomy (FsError with an FS_* code), never a bare Error.
  3. Add a unit case for the pure part and an E2E case for the round-trip.
  4. If it mutates, invalidate the realpath cache and take the per-target lock.

Performance notes

Every remote operation is at least one round trip. Connection multiplexing (ControlMaster=auto + ControlPersist) makes the second call cheap, so the rule is: one round trip per user-visible operation, not one per internal step. listDir is a single NUL-delimited scan for that reason, and realpath results are cached for two seconds and invalidated on every mutation.

5. How changes land

main is protected: it takes pull requests, not pushes, and every required check must be green first. The flow is

git checkout -b fix/short-description
# ...work...
git push -u origin fix/short-description
gh pr create --fill

Three workflows post a stable check name that main requires — ci-gate, e2e-gate, and portability-gate — each of which aggregates its own matrix so branch protection needs one context per workflow instead of every expansion.

The PR bot (.github/workflows/pr-bot.yml) then analyzes the pull request, comments a per-area summary, and arms GitHub's native auto-merge (squash) only when the author is a trusted actor and the change touches neither .github/workflows/ nor install.sh — those two run with repository permissions and deserve a human read. External contributions always wait for a maintainer.

The bot runs on pull_request_target, which is the only trigger that sees fork PRs with a write token. It therefore never checks out, imports, or executes the pull request's code: it reads the API only. Anything that needs the PR's files belongs in .github/workflows/ci.yml, which runs with a read-only token.

6. Pull requests

  • One concern per PR; a behaviour change gets a test in the same PR.
  • Conventional commit subjects (feat:, fix:, test:, docs:, ci:).
  • Say in the description which suite you ran and against which server, including which userland (Alpine, Debian, macOS) when it matters.
  • If you touched the transport, say which ssh version you tested with.

7. Reporting a bug

Include:

  • ssh -V, node -v, the harness version (dsh -V), and the OS of both ends;
  • the exact error text (the plugin tries to make it actionable — please quote it);
  • whether ssh <host> works by hand from the same machine;
  • the uname -a of the server, and whether rg, realpath, and stat exist there.

Do not paste secrets, private keys, or remotes.json.

8. License

By contributing you agree your work is released under the MIT License.