Jesse-njx/dsh-docker
dsh-docker — typed, guarded container control for DSH: structured docker/compose tools, project-aware targeting, an approval gate for destructive ops, service-health context, and a replayable status-table renderer
Listed
1
Tools
Bundle verified
What it does
Typed, guarded container control: ps/logs/inspect/exec/start/stop and compose up/down with JSON output, project-aware targeting, and approval-gated destructive ops.
Best for
- DSH workflows that inspect and troubleshoot Docker containers or Compose services using structured JSON.
- Projects that need Compose-aware targeting so container actions remain scoped to the detected project.
- Teams that want human approval before destructive container, image, volume, or Compose operations.
Not ideal for
- Hosts whose Docker CLI lacks `ps --format json` support or whose Compose installation is not v2.
- Unattended workflows that must perform destructive operations without an approval channel; guarded operations fail closed.
- Workflows that need normalized interactive terminal output, because interactive exec may retain CRLF and ANSI framing.
README
dsh-docker
Typed, guarded container control for DSH — structured Docker access that is hard to destroy by accident.
dsh-docker is a plugin bundle for DeepSeek Harness. It wraps the docker CLI through DSH’s ctx.shell seam and gives your agent a typed, project-aware, guarded surface for containers and compose stacks — every tool returns structured JSON (await tools.docker_ps(...) gets real objects, never scraped prose), destructive operations require human sign-off through the approval gate, and stop dev-api means your local compose service, not a coincidentally-named container on the machine.
┌──────────────┐ docker ps --format json ┌──────────────┐
│ agent tool │ ───────────────────────────▶ │ ctx.shell │ ──▶ docker CLI
│ call │ │ (sandbox/ │
└──────┬───────┘ │ remote) │
│ classify() └──────────────┘
▼
┌──────────────┐ destructive? ┌──────────────────────┐
│ pre-execute │ ──────────────▶ │ approval gate │ allowed-once → run
│ policy │ (guarded) │ (ctx.approval, │ rejected/unavailable
└──────┬───────┘ │ fails closed) │ → refused
│ safe / token recorded └──────────────────────┘
▼
┌──────────────┐ hard-destructive without a token? → refused (monotonic)
│ ctx.tools │
│ .guard() │ backstop: no later listener can undo a denial
└──────────────┘
Flagship demo — debug a failing integration test
Your agent runs a failing test against docker-compose.yml:
$ docker_ps → { containers: [{ name: "app-api-1", state: "running", ... }] }
$ docker_logs { container: "app-api-1", tail: 200 }
→ { lines: [...], truncated: true } # capped pull never reads as complete
$ docker_compose_ps → { project: "app", services: [{ name: "db", state: "running" }] }
The agent reads freely. The moment it tries something destructive — docker_rm -f on the running API container, docker_rmi on an image a container still uses, docker system prune -a, or docker compose down -v — the call is paused and routed to the human approval gate:
⏸ docker_rmi "app-api:latest" — image in use by container app-api-1
[approve] [reject]
No approval channel, or a denied request? The operation is refused. execReadOnly is on by default, so docker_exec without write/interactive never prompts, and a writeful exec is treated as destructive. And even if some other plugin’s policy said “allow”, the monotonic guard still refuses the hard-destructive set unless this call carries the dsh-docker approval token.
Install
# From the npm registry once published, or straight from this repository:
dsh plugin --profile web add @dsh-docker/bundle # npm (when published)
dsh plugin --profile web add github:Jesse-njx/dsh-docker # or: straight from GitHub
Install into whatever profile your agents run under (web for the desktop UI, headless for CLI sessions). The bundle mounts the tools, the policy, the opt-in health context, and the web status renderer.
Tools
Every tool is a defineTool with typed parameters and a structured output schema — Code Mode sees await tools.<name>(...) returning the canonical JSON value, never rendered prose.
Containers
| Tool | Output | Notes |
|---|---|---|
docker_ps |
{ containers: [{ id, name, image, state, status, ports[], project?, service? }] } |
--all flag; scoped to the detected compose project by default |
docker_logs |
{ lines: string[], truncated } |
bounded pull (tail, default 200, cap 5000); pulls tail+1 so truncated is honest |
docker_inspect |
raw engine JSON (open object) | the adapter — engine-native structure, validated |
docker_exec |
{ exitCode, stdout, stderr } |
read-only by default (no TTY); write/interactive reclassifies into the approval bucket |
docker_start / docker_stop / docker_restart
|
{ affected: string[] } |
one ref, or the whole detected project with no ref |
docker_rm |
{ affected: string[] } |
guarded when the target is running or force is set |
Images
| Tool | Output | Notes |
|---|---|---|
docker_images |
{ images: [{ id, repository, tag, size, inUse }] } |
inUse from a docker ps -a cross-reference |
docker_rmi |
{ removed: string[] } |
guarded when the image is in use |
docker_prune |
{ scope, all, volumes, ok } |
guarded for --all, --volumes, or system scope |
Compose
| Tool | Output | Notes |
|---|---|---|
docker_compose_up |
{ project, services, detached } |
detached by default; optional services[]
|
docker_compose_down |
{ project, affected, volumes } |
guarded when volumes: true (the -v flag) |
docker_compose_ps |
{ project, services: [{ name, id, state, status, ports }] } |
one read builds the health context |
Project-aware targeting
Every tool resolves the compose project once per call (a pure, unit-tested ProjectResolver):
- Walk up from the tool’s working directory for the first
composeFilesmatch (docker-compose.yml,compose.yaml, config-ordered). - Derive the project name (directory basename, lowercased) and pass
-p <project> -f <file>to every compose call;docker_ps/start/stopdefault filters scope to--filter label=com.docker.compose.project=<project>. - Every tool accepts an explicit
project: stringoverride; with no compose file and no override, container-scoped tools operate globally but the project-aware behaviors are off.
A bare container name resolves within the project first, falling back to a global match only when unambiguous. Ambiguous refs return an error listing candidates instead of guessing.
Guardrails
Two layers, both in the tools pipeline:
-
tools/pre-executepolicy —classify(toolName, args, resolved)(a pure function, unit-tested without a daemon) judges each call’s intended effect. Guarded ops route a one-shot approval request throughctx.approval(approval/requestwaterfall); approval absent fails closed tounavailable⇒ refused. A guarded op not covered by the configuredapprovalglobs is refused outright. -
ctx.tools.guard()backstop — a monotonic deny that a later listener cannot undo. Even if another plugin’s pre-execute said “allow”, the guard refuses the hard-destructive set unless this call carries the dsh-docker approval token.
Read-only docker_exec is free by default; execReadOnly: false flips that for operators who accept the risk.
Service-health context (opt-in)
plugins:
dsh-docker:
healthContext: { enabled: true, maxServices: 12 }
When enabled, each pre-step injects one compact line per detected compose service — dev-api ▲up dev-db ▲up worker ▼exited(1) — as durable appended context the next model request sees (not a wake-up). Built from a single docker_compose_ps read, TTL-cached, capped at maxServices, and skipped entirely when no compose project is detected.
Status renderer (web client)
The client half registers a replayable conversation node for docker_ps / docker_compose_ps / docker_logs output: one stable node per tool call (keyed by callId), a table collapsed by default and expandable on click, with a log pane whose truncated flag is surfaced so a capped pull never reads as complete. Replay-safe: the renderer consumes only the durable tool/result presentation metadata, never the execution-local value.
Config
plugins:
dsh-docker:
enabled: true
composeFiles: [docker-compose.yml, compose.yaml]
approval: ["rmi:*", "rm:*", "prune:*", "compose down -v", "exec:*"]
execReadOnly: true
timeoutMs: 30000
healthContext: { enabled: false, maxServices: 12 }
| Field | Default | Meaning |
|---|---|---|
enabled |
true |
master switch |
composeFiles |
[docker-compose.yml, compose.yaml] |
config-ordered discovery list |
approval |
["rmi:*", "rm:*", "prune:*", "compose down -v", "exec:*"] |
ops routed to the approval gate (* matches any args; compose down -v matches only volume-removing downs) |
execReadOnly |
true |
false runs docker_exec ungated |
timeoutMs |
30000 |
per docker invocation timeout |
healthContext.enabled |
false |
inject per-service health lines before each step |
healthContext.maxServices |
12 |
cap on health lines (+ N more tail) |
The same schema registers as the dsh-docker user-settings section, so a live edit reaches the very next tool call.
Non-goals (v0.1)
Docker contexts / remote-host management UI; building images from a Dockerfile with any DSL (raw docker build passthrough is fine — no wrapping); swarm / k8s; container runtime introspection beyond docker inspect; log streaming / follow; registry auth flows. Each is a v0.2+ question, not a config flag away.
Testing
-
Unit (no daemon): arg-schema acceptance/rejection per tool;
ProjectResolverfixtures (compose-file discovery, project-name derivation, ambiguous-ref handling); theclassify()policy matrix — every destructive op assertsguarded, every safe op assertssafe, theexecReadOnlytoggle flipsdocker_exec; the approval-glob matcher. -
Guard (no daemon): a mock approval gate that always denies —
rmiin-use,rmrunning,prune --all,compose down -v, and writefulexecare refused; an approving mock — they proceed exactly once; a preempting “allow” from another listener is refused by the backstop. -
Integration (feature-detected): probes for a live daemon (
docker version) and, when present,compose up/ps/downa tiny alpinesleepstack with a mock approval gate — typed rows match reality anddown -vrequires approval. Skips cleanly (marked skipped, not failed) when no daemon is available.
pnpm install
pnpm typecheck && pnpm build && pnpm test
Model Experience
Request context and condition
What the model sees
The 14 tool schemas (names, descriptions, parameters) flow into system-prompt assembly through the tool registry, exactly like every other registered tool; there is no additional fixed prompt prose owned by this package. When healthContext.enabled is on and a compose project is detected in the agent’s working directory, each pre-step injects a source: { kind: 'plugin', plugin: 'dsh-docker' } user message:
[dsh-docker] <project> services: dev-api ▲up dev-db ▲up worker ▼exited(1)
Token effect
The tool schemas are fixed prompt tokens owned by the registry. The health line is conditional: bounded to maxServices lines plus a +N more tail, present only while the feature is enabled and a compose project is detected.
KV Cache effect
Append-only in the sense that each step appends a fresh health message that the next request consumes and discards; the message text is data-dependent (service states change), so a stable prefix is preserved but the tail invalidates reuse whenever a service state changes. Package-owned changes that can invalidate reuse: none at runtime — a reinstall/upgrade of the bundle changes the schemas, which is a normal dependency-change invalidation.
Known Limitations and Deferred Work
-
docker_ps/docker_compose_psneed the docker CLI to support--format json(Docker ≥ 25 forps, compose v2 for composeps). Older CLIs surface a clear error rather than scraped text; thedocker inspectbatch is the adapter where the psLabelsfield is absent. -
Failed tool calls render no status table —
presentationMetais computed from successful canonical values only, so a faileddocker_psshows the standard error card, not a table. The durable log carries no canonical value by design. -
docker_prunecannot report what the engine reclaimed without scraping human prose; the canonical output carries the request and success, and the engine’s reclaimed-space line lives in the rendered text only. -
docker_exec --interactiveoutput may carry TTY framing (CRLF/ANSI); the canonical value returns it verbatim rather than laundering it.
Frequently Asked QuestionsFAQ
Use the verified command dsh plugin --profile default add github:Jesse-njx/dsh-docker in a DSH-enabled shell. The command resolves the public package metadata and keeps the plugin attached to the catalog identity shown on this page.
Compatibility follows the bundle and profile status shown above. If a profile is not detected, keep the plugin disabled there and check the repository documentation before enabling it in production.
The GitHub link and activity metadata are the source of truth for releases and maintenance. Revisit this page after a new release to confirm the catalog has observed the latest version.