Imported from zhming0/dsh-yawn (
AGENTS.md). Install upstream withnpx skills add zhming0/dsh-yawn. Copyright stays with the author.
Repository guidance
Learn what dsh is before you change anything
dsh is DeepSeek Harness, an agent harness that shipped in 2026 as a pre-1.0 developer preview. It is almost certainly not in your training data, and there is little public documentation. Do not guess its API or its behavior. Every claim you make about dsh should come from a file you read.
The authoritative documentation is each npm package's own README, and those READMEs are unusually detailed and precise. Read them directly:
cd "$(mktemp -d)"
npm pack @deepseek-ai/dsh-base@0.1.7-rc.2
tar xzf *.tgz
# package/README.md is the spec. package/lib/*.js is the built source, which is
# readable and worth grepping when a README leaves a detail open. A bundle also
# carries package/cordis.patch.yml, the rows it contributes.
This repository pins 0.1.7-rc.2. Match it, because the surface moves between
releases.
The model
dsh is a Cordis plugin
tree, composed at boot. Plugins publish services under a name (fs, shell,
subprocess, agents) and consume them with static inject. This package
supplies its own implementations of three of those services, so dsh's built-in
tools use a sandbox without knowing one exists.
| Term | What it means |
|---|---|
| Profile | One installed tree, at $DSH_HOME/profiles/<name>/ ($DSH_HOME defaults to ~/.dsh). Holds package.json with the ordered dsh.profile.bundles list, plus your cordis.patch.yml. |
| Bundle | An npm package declaring dsh.bundle.patch. Its patch file becomes a layer. This package is one. |
| Patch layer | A YAML list of {id, name, config} rows plus insert: and disabled:. Layers apply over an empty list: bundles in order, then the profile's cordis.patch.yml, then $DSH_HOME/cordis.patch.yml, then --patch. |
| Agent preset | A per-session composition, picked in the Web UI. Since dsh 0.1.7 presets are ordinary patch rows (dsh-web-app/presets/*.patch.yml). Different from a profile. |
| Isolate realm | cordis:group with isolate: gives a subtree its own copy of named services. A preset must use one for any service it publishes. |
Which package answers which question
| Question | Read |
|---|---|
Profiles, dsh plugin, launcher flags |
@deepseek-ai/dsh |
Patch layer precedence, $DSH_HOME, boot |
@deepseek-ai/dsh-app-boot |
| Which row ids exist, and their defaults | @deepseek-ai/dsh-base (read cordis.patch.yml) |
| What each surface changes | @deepseek-ai/dsh-web-app, @deepseek-ai/dsh-headless |
| Per-session compositions (preset rows) | @deepseek-ai/dsh-web-app (presets/*.patch.yml) |
| Config forms over row Configs | @deepseek-ai/dsh-settings |
| The interfaces this repo implements | @deepseek-ai/dsh-fs, -shell, -subprocess |
How a Web session is created, including its cwd |
@deepseek-ai/dsh-api-session-controller |
Verified facts that contradict a reasonable guess
Each of these was checked against the packages above. They are the ones most likely to mislead you.
dsh plugin --profile <n> add <pkg>ispnpm addin the profile directory and nothing more. A package withoutdsh.bundle.patchinstalls as a plain dependency and wires up nothing.- A patch entry replaces the matched row's whole
config. It does not deep merge, so an override must restate every field it keeps. dsh webstarts a server and prints a URL. It does not start a session; sessions are created from the browser.- The printed URL carries a per-process
?token=.GET /without it, or without the cookie it mints, returns 401; static assets stay public. The cookie is bound to theHostthe browser used and is signed with a secret kept in$DSH_HOME/.credentials.yaml, so it survives host restarts. - A session's working directory falls back through selected workspace, then
request payload, then
process.cwd(). It is always set, and dsh creates the directory if it is missing. shellis built on top ofsubprocess, sosubprocessis the seam that decides where a command actually runs.tool-fs-search(glob,grep) injectssubprocess, notfs, and spawns a ripgrep binary resolved from the dsh host'snode_modules, with the session cwd and the model's search root in host coordinates. On its own it cannot work against a remote filesystem; this repository's sandbox subprocess seam translates the workdir, session-frame argv paths, and host-only executables so the stock row runs inside the sandbox.- On the Web surface, tool rows are mounted by agent presets, and a preset
row's
name:is the module that loads. Since dsh 0.1.7 presets are ordinary patch rows (preset-standardand friends, shipped asdsh-web-app/presets/*.patch.ymlbehind anagent-preset-registryrow), a bundle patch can restate one — but this repository still does not: the shipped presets restate tool rows by their stock names, and working through the three capability seams keeps that true. Web-editor preset changes persist into the profile's owncordis.patch.yml, the same file the image seeds once, and a session pinned to a preset id that no longer exists fails to resume — keep preset ids stable. - A plugin reaches the Web settings surfaces through the volatile fields of
its row Config (dsh 0.1.7's Config forms) plus, for a hand-written page, the
settings.sectionslot. Namespace registration no longer exists. - A settings-form write lands in the profile patch, but a home patch
(
$DSH_HOME/cordis.patch.yml) or--patchoverlay outranks it, and the config editor refuses a write they would override. So a chart cannot deliver the sandbox-manager row as a home patch: the Web page cannot save, and the legacysettings.yamlimport silently skips that row (renaming the file tosettings.yaml.imported). Deployment values instead arrive as an ordinary base file the control plane reads,/etc/dsh-yawn/sandbox-settings.yaml, whose top-levelsandboxManagersection is the chart-to-image contract: the image tag can lag the chart's, so unknown sections are ignored and the file stays specific to this package. The deployment owns the profile names it defines; see control-plane/README.md. - A patch entry's
name:is an assertion, not a rename. When it differs from the matched row's module,dsh-app-bootskips the entry with a warning and the stock row stays as it was. - The Web app serves a package's browser half (
dsh.clientin itspackage.json) only while a row of that package is mounted. Disablingworkspace-filesalone leaves the sidebar's Files and Preview tabs without their file resource provider, so the tab rows go with it. To change what a stock host service does, keep its row and wrap the live instance.
Write code people can maintain
- Prefer plain English, direct names, and small functions with one clear job.
- Make the smallest change that fully solves the problem. Avoid speculative configuration, wrappers, and abstractions.
- Keep comments for decisions and non-obvious constraints. Do not narrate code that is already easy to read.
- Follow the existing style in the package you are changing.
Respect the system boundaries
control-plane/owns session lifecycle, credentials, and the Docker and Kubernetes backends.runner/owns commands and file operations inside one sandbox.proto/is the source of truth for the ConnectRPC contract between them.- The runner dials out to the control plane's tunnel listener and authenticates with the shared registration token; RPCs then flow control-plane→runner over that runner-initiated connection. Do not add a listener on the runner for the control plane to dial, and do not give the runner any other channel back to the control plane.
- Keep control-plane paths and sandbox paths separate. Model-facing file and shell work must resolve inside the session's sandbox workspace.
- Preserve lifecycle meaning: hibernation keeps workspace data, wake reuses it, and expiry removes it.
Preserve the supported product
- The product is the Kubernetes distribution: the control-plane and runner images are released together. Docker and checkout installs are development paths.
- Only
dsh webis supported. Headless mode exits before the idle lifecycle can run. - The
webprofile on the data volume is user data as well as image data: an image upgrade merges the image's manifest fields and refreshes its own package, and keeps plugins installed from the Web Plugins page. Installed bundles run in the control-plane process, outside every sandbox, so the page is operator access. - One control plane is one trust domain. Its sessions, credentials, and sandboxes are not isolated from other users admitted to that control plane.
- Secrets are global to the control plane and are pushed to a runner before
commands.
GITHUB_TOKENalso supplies Git credentials for github.com. There is no GitHub device flow or per-repository secret scoping. - Sandbox code can read injected secrets by design. Keep credentials in the
control-plane store, never in pod configuration or workspace files. Two are
deliberate exceptions, because a sandbox must never receive them: the shared
registration token, which lives in the
dsh-yawn-registration-tokenSecret because warm pods must hold it before any session exists (it only lets a runner register a tunnel, and grants nothing else), and a Buildkite API token, which the control plane resolves for its own Buildkite calls from the process environment or the host credential document (write-only from the Sandboxes page) because it can create and cancel builds. - The Kubernetes backend uses cluster-owned templates and warm pools. The operator publishes them as named sandbox profiles; a session picks a profile, never pod privileges or an arbitrary template.
Protect credentials and generated code
- Never write tokens or secret values into a workspace, log, test fixture, or committed file. The control plane stores credentials; the runner holds delivered values in memory.
- Do not hand-edit generated protobuf files. Change
proto/and runpnpm proto:generate. - The pinned dsh, agent-sandbox, Go,
jj, andmiseversions are intentional. Update them only as part of an explicit compatibility change. mise.tomlis the only place CI and local development read tool versions from. Do not reintroduce a version in a pipeline image, a step script, or the README. pnpm is the exception:packageManagerinpackage.jsonpins it.
Inspect Buildkite CI
Use bk, pinned in mise.toml, to inspect this repository's CI in the
zhming0 organization and dsh-yawn pipeline. Prefer the pre-provisioned
BUILDKITE_API_TOKEN; it takes precedence over the credential store and legacy
config, and BUILDKITE_ORGANIZATION_SLUG selects the organization, so no login
is needed when they are set. Do not run bk auth login --device or any
interactive login unless the user explicitly asks. If the token is absent,
report that and stop. When explicitly asked to log in from a headless sandbox,
run bk auth login --device --credential-store shm; then confirm with
bk auth status.
Test the affected path
For TypeScript or protobuf changes, run:
pnpm check
pnpm lint
pnpm format:check
pnpm test
pnpm build
For runner changes, also run:
(cd runner && go test -race ./... && golangci-lint run && go build ./...)
Run pnpm test:docker for Docker lifecycle, runner protocol, setup, credential,
or sandbox tool changes. Run pnpm test:kas for control-plane-to-runner transport, Kubernetes
lifecycle, or manifest changes. Regenerate protobuf code and confirm
there is no generated-code diff when the contract or generator settings change.
Follow docs/e2e-testing.md for the complete Docker,
Kubernetes, and browser/model acceptance workflows, including expected results,
cleanup, and failure investigation.
Update the relevant README or docs/ page when behavior, configuration,
security boundaries, setup, or supported limits change.
