@hyzyn/dsh-docker 0.9.5 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +157 -31
- package/README.md +146 -30
- package/client-src/docker.css +343 -160
- package/client-src/elevation.js +163 -0
- package/client-src/index.js +563 -119
- package/client.js +367 -183
- package/lib/audit.d.ts +106 -0
- package/lib/audit.js +140 -0
- package/lib/audit.js.map +1 -0
- package/lib/docker.d.ts +32 -3
- package/lib/docker.js +43 -7
- package/lib/docker.js.map +1 -1
- package/lib/index.d.ts +53 -0
- package/lib/index.js +473 -25
- package/lib/index.js.map +1 -1
- package/lib/ssh-exec.d.ts +9 -0
- package/lib/ssh-exec.js +149 -16
- package/lib/ssh-exec.js.map +1 -1
- package/package.json +2 -2
- package/scripts/client-smoke.mjs +394 -0
- package/scripts/log-perf.mjs +9 -2
- package/scripts/route-smoke.mjs +216 -5
package/README.en.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **A resident session right-sidebar tab**: the panel lives as a session right-sidebar tab (`sidebar.right.pane.tab`) **side by side** with the conversation — after handing an error to the agent the logs stay on the right, without getting in the way of watching it work; collapsing it leaves the viewport. Panel-level state (target / view / filter text / selected container and its tab) is kept **across sessions**, and collapsing drops the live streams to hand the SSH channels back. Hosts without the right-sidebar services fall back to the original dock / modal, behaving exactly as before.
|
|
10
|
+
- **An explicit one-click "connect local"**: two **non-permanent** entries — with no targets at all it is the primary button in the "no Docker targets configured yet" empty state, and once you have other targets it is the **last item in the target dropdown**, `+ Connect local` (it is deliberately **not** on the toolbar, where being permanent means occupying space and begging for clicks — pure noise) — one click (or picking that item) adds the machine hosting the plugin as a local target and selects it (an existing local target is reused, never duplicated; SSH and custom targets stay untouched), and the same operation is `docker_connect_local` on the agent side. It does **not** change the meaning of an empty `targets`, never invents an implicit default target, never installs Docker / starts a daemon / switches docker contexts, and needs no capability grant (it only adds one local target pointing at the plugin's own host).
|
|
10
11
|
- **Aggregated fetches across targets**: the Overview page fans out over every `targets[]` entry in parallel, and an unreachable target only spoils its own cell; the agent side exposes the same shape through `docker_ps target:"*"` / `docker_attention target:"*"`, so targets never block one another.
|
|
11
12
|
- **"Needs attention" reads authoritative fields**: unhealthy / repeatedly restarting / OOM-killed / non-zero exit / dead; OOM and the real exit code come from one `docker inspect` — the 137 in a `docker ps` summary cannot separate an OOM kill from a manual kill, so filtering on the summary alone must misreport.
|
|
12
13
|
- **Four long-lived SSE streams on one substrate**: log FOLLOW, `docker stats`, `docker events` and `docker pull` all run through the same `openSseStream` (heartbeat / active-stream registry / teardown on disconnect) and differ only in how they end — logs and pulls finish on their own, stats and events are aborted by the browser. Multi-select merged logs recover true cross-container ordering from the `--timestamps` prefix; "pause" freezes rendering only (the stream keeps receiving and flushes in one batch on resume).
|
|
@@ -191,6 +192,15 @@ Inside the panel:
|
|
|
191
192
|
health_status / destroy / rename / update): noise such as `exec_*` and `archive-path` (`docker cp`)
|
|
192
193
|
is dropped server-side — on one batch machine 47 events over 24 hours were all exec with 0 allowlist hits, so on
|
|
193
194
|
machines that are only ever exec'd the Activity strip is empty, and that is deliberate.
|
|
195
|
+
A closed stream needs a **manual reconnect**: entering `closed` is terminal (EventSource only self-heals
|
|
196
|
+
connection blips; the `docker events` process exiting or the server sending an `end` frame both close it), so the
|
|
197
|
+
Activity strip offers a **Reconnect** button (shown only while `closed`; not in `unsupported` environments, where
|
|
198
|
+
reconnecting would just fail again), and the toolbar's ⟳ reconnects as well. The **AUTO REFRESH poll deliberately
|
|
199
|
+
does not reconnect**: folding it into the poll would reopen a terminated stream forever, both hiding that it broke
|
|
200
|
+
and pointlessly creating SSE connections. A successful reconnect compensates with one full list refetch (the list
|
|
201
|
+
may have fallen behind while the stream was down), including the case where the daemon never came up and the
|
|
202
|
+
stream never opened at all — otherwise the strip turns green while the list still shows its error state, looking
|
|
203
|
+
fixed when it is not.
|
|
194
204
|
- **Select for merging (temporary multi-select merged logs)**: the toolbar's `Select for merging` enters selection
|
|
195
205
|
mode — a checkbox appears on the left of every card, clicking a card body becomes **select / deselect** (it no
|
|
196
206
|
longer opens details; the action bar collapses temporarily so that multi-selecting does not mis-click
|
|
@@ -377,6 +387,70 @@ must satisfy: the docker CLI is installed, and the current account can use docke
|
|
|
377
387
|
**without sudo** (usually because it is in the `docker` group); otherwise `probe` passes through errors such as
|
|
378
388
|
`permission denied while trying to connect to the Docker daemon socket` verbatim.
|
|
379
389
|
|
|
390
|
+
#### Connect local (one click)
|
|
391
|
+
|
|
392
|
+
If you would rather not fill in a target by hand, the UI offers two entries, neither of them
|
|
393
|
+
permanent:
|
|
394
|
+
|
|
395
|
+
| When | Where |
|
|
396
|
+
| --- | --- |
|
|
397
|
+
| You have no targets at all | A **Connect local** primary button in the panel's empty state (the whole panel is blank then, so one click is the shortest path) |
|
|
398
|
+
| You already have other targets | The **last item in the target dropdown**, `+ Connect local` — right where you look for a machine, and only visible once you open it |
|
|
399
|
+
|
|
400
|
+
The two are mutually exclusive (the same entry never appears twice): with no targets you use the big
|
|
401
|
+
button in the empty state; with targets, the dropdown carries the item. **Once a local target exists
|
|
402
|
+
neither appears** (it is already in the selector, so listing it again would just be a duplicate).
|
|
403
|
+
|
|
404
|
+
On the agent side the same operation is `docker_connect_local`, and over HTTP it is
|
|
405
|
+
`POST /connect-local` — every entry point shares one implementation on the host.
|
|
406
|
+
|
|
407
|
+
Why not a permanent toolbar button: a permanent "Connect local" both occupies the spot right of the
|
|
408
|
+
target selector and has to attract clicks with an accent colour, while being pure noise **once you
|
|
409
|
+
have a usable target** (the first version was permanent, and the user called it out as redundant on
|
|
410
|
+
sight). But keeping only the empty-state entry would miss the common case of "I have a few SSH hosts
|
|
411
|
+
and want to add this machine too" — so the second entry lives in the dropdown, appearing only when
|
|
412
|
+
you are genuinely looking for a machine.
|
|
413
|
+
|
|
414
|
+
| Situation | Behavior |
|
|
415
|
+
| --- | --- |
|
|
416
|
+
| A `kind=local` target already exists (whatever its name) | **Reuse it** (`created:false`) and just switch to it; nothing is created, rewritten or renamed |
|
|
417
|
+
| No local target | Append one under the stable name `local`; if `local` is taken by another target, try `local-2`, `local-3`, … (deterministic, conflict-free, never overwriting) |
|
|
418
|
+
| SSH / custom targets configured | **Left exactly as they are** (order and credentials included); only the local entry is appended at the end |
|
|
419
|
+
| Clicked twice / concurrently | Idempotent: exactly one local target in the end |
|
|
420
|
+
| Settings store unavailable / save fails | A clear error (503 / 500 with the reason) and **no in-memory config change** — never "it showed up in the UI but is gone after a refresh" |
|
|
421
|
+
|
|
422
|
+
After the write succeeds it also runs one **read-only probe** (`docker version`) and puts the result
|
|
423
|
+
into `message`: the server version when the daemon is reachable, or the actual cause when it is not —
|
|
424
|
+
**docker CLI missing** (install Docker, or point "docker CLI" at the right path) / **probe timed
|
|
425
|
+
out** (the daemon is wedged, or `docker context` points at an unreachable remote; check with
|
|
426
|
+
`docker context ls`) / **daemon not running** (Linux: `systemctl start docker`; macOS / Windows:
|
|
427
|
+
start Docker Desktop) / **no permission on the socket** (add the account to the `docker` group) —
|
|
428
|
+
instead of just "no containers". A failed probe is **not** a failed operation (the target really was
|
|
429
|
+
added; `reachable:false` says so separately), and it works as soon as the daemon comes up.
|
|
430
|
+
|
|
431
|
+
The binary this probe uses is **exactly the "docker CLI" value from the settings card** (`dockerBin`,
|
|
432
|
+
`docker` by default). When that CLI is missing it does one **read-only** lookup for another container
|
|
433
|
+
CLI on `PATH` (`docker` → `podman` → `nerdctl`) and names it in the message ("this machine has
|
|
434
|
+
podman: put podman in 'docker CLI' and it works") — it **only tells you, and never rewrites your
|
|
435
|
+
config**: silently changing `dockerBin` would contradict this plugin's refusal to decide on your
|
|
436
|
+
behalf, and podman's output-format compatibility with this plugin has not been verified item by item
|
|
437
|
+
(see "known limitations"). Candidate matching is by **tool name** (basename), not by the raw string
|
|
438
|
+
in the config, so a `dockerBin` of `/usr/local/bin/docker` does not produce the nonsense "change
|
|
439
|
+
docker to docker".
|
|
440
|
+
|
|
441
|
+
Timeouts are a **separate case**: `probe()` does not just report an exit code (`code === null` makes
|
|
442
|
+
"exit code null" carry zero information), it returns `timedOut:true` plus an actionable sentence. It
|
|
443
|
+
also **actually returns on time** — SIGKILL only reaches the direct child, so when `dockerBin` points
|
|
444
|
+
at a wrapper script the grandchild keeps holding the pipes and stretches the 15-second cap into
|
|
445
|
+
minutes (measured: a 3-second cap took 60 seconds), so on expiry it kills, allows a 500 ms settle
|
|
446
|
+
window, and then releases the pipes itself if `close` still has not arrived.
|
|
447
|
+
|
|
448
|
+
Deliberately **not** done (the conservative boundary): installing Docker, starting a daemon,
|
|
449
|
+
switching docker contexts, or touching any environment variable; an empty `targets` (= the user has
|
|
450
|
+
none) keeps its meaning, with no implicit default target; and the `clearTargets` protection against
|
|
451
|
+
accidental clearing is untouched. The path is also **independent of the capability switches** — it
|
|
452
|
+
only writes the plugin's own configuration and involves no target-side write that would need a grant.
|
|
453
|
+
|
|
380
454
|
### Merged logs (multi-select / Compose project)
|
|
381
455
|
|
|
382
456
|
Selecting several containers or opening a Compose project can both merge the logs of several containers into one
|
|
@@ -460,10 +534,10 @@ return 400).
|
|
|
460
534
|
| --- | --- | --- |
|
|
461
535
|
| `enabled` | true | disables the whole plugin (**takes effect on save**: tools are unregistered immediately, the announcement is withdrawn, and all data routes except `/config` return 403; `/config` stays readable and writable — the settings card is the way back in. Unlike tty, which needs a restart) |
|
|
462
536
|
| `announceToAgent` | true | whether to inject a capability announcement into the agent (systemPrompt section `plugin:dsh-docker`) |
|
|
463
|
-
| `dockerBin` | `docker` | the docker CLI executable name or path (`podman` works here); only letters, digits and `_ . / \ : -` plus interior spaces are allowed, and it may not start with `-` (**Windows drive letters and `\` must be allowed**, otherwise no absolute path can be entered at all) |
|
|
537
|
+
| `dockerBin` | `docker` | the docker CLI executable name or path (`podman` works here); only letters, digits and `_ . / \ : -` plus interior spaces are allowed, and it may not start with `-` (**Windows drive letters and `\` must be allowed**, otherwise no absolute path can be entered at all). **"Connect local" probes with exactly this value**; when it is wrong (that CLI does not exist) the message names another container CLI found on `PATH`, but it does **not** rewrite this value for you |
|
|
464
538
|
| `allowMutations` | false | allows **mutating operations**: container start / stop / restart / remove, image removal / dangling pruning / pulling (the panel buttons and the `docker_action`, `docker_image_remove`, `docker_image_prune`, `docker_image_pull` tools; while off, `/action`, `/images/remove`, `/images/prune`, `/images/pull/stream` return 403 and the corresponding tools are not registered) |
|
|
465
539
|
| `allowExec` | false | allows a one-shot `docker exec` (the panel's exec input and the `docker_exec` tool; while off, `/exec` returns 403) |
|
|
466
|
-
| — (capability grant) | not granted | `allowMutations` / `allowExec` have **two out-of-band channels**: ① **launch environment variables** (`DSH_DOCKER_ALLOW_MUTATIONS` / `DSH_DOCKER_ALLOW_EXEC`, values `1` / `true` / `yes` / `on`) — resolved from the host's **launch snapshot**, inherited `process` layer only, so writing project `.env` or `~/.dsh/env.yml` does **not** count as a grant; ② **grant in place** (no restart): click the switch in the settings card and run the command it shows in a terminal on the host — effective within seconds, on its own row (one row per capability — the grants are per capability, so side-by-side revoke buttons would not say which one belongs to which), next to a `Granted · YYYY-MM-DD HH:mm:ss` line and a `Revoke host grant` button (a launch-environment grant has no timestamp and cannot be revoked from the UI). In-place grants are **persistent**: they live in `<DSH home>/dsh-kit/capability-grants.json` (0600; the confirmation dir is `<DSH home>/dsh-kit/grant-confirm/`, 0700 — kept inside the kit's own subdirectory, because mechanism names dropped into the shared home root can collide with the harness or another plugin), are read at the next boot, and then **take effect with no further confirmation** — so every boot logs one `elevation: load capability=… via=file grantedAt=…` line per loaded grant. Over HTTP they can always be **turned off** (the emergency brake must not depend on a restart), but setting `true` without a grant is rejected with 400 and both routes spelled out. A `true` in the config is **not** a grant (it lives in the same store HTTP writes to, so the source is indistinguishable). **Upgrade note**: switches opened through the UI before this change become off — set the variable and restart, or grant in place. **Why**: neither the loopback fence nor the same-origin proof stops cross-site pages or in-page scripts (they can just set `Sec-Fetch-Site: same-origin`), and the docker socket is root on the target host; details (including whom this does *not* stop) in [architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么) |
|
|
540
|
+
| — (capability grant) | not granted | `allowMutations` / `allowExec` have **two out-of-band channels**: ① **launch environment variables** (`DSH_DOCKER_ALLOW_MUTATIONS` / `DSH_DOCKER_ALLOW_EXEC`, values `1` / `true` / `yes` / `on`) — resolved from the host's **launch snapshot**, inherited `process` layer only, so writing project `.env` or `~/.dsh/env.yml` does **not** count as a grant; ② **grant in place** (no restart): click the switch in the settings card and run the command it shows in a terminal on the host — effective within seconds, on its own row (one row per capability — the grants are per capability, so side-by-side revoke buttons would not say which one belongs to which), next to a `Granted · YYYY-MM-DD HH:mm:ss` line and a `Revoke host grant` button (a launch-environment grant has no timestamp and cannot be revoked from the UI). **Both capabilities can be confirmed with a single paste**: clicking the second switch while the first panel is open no longer replaces it (the two grants are independent anyway) — the panels stack vertically and a **Copy all (N)** button appears at the top of the group, copying the still-pending commands in the order they were started, joined by a **newline**, so one paste in a host terminal runs them all (`bash` / `zsh` / PowerShell / cmd all run a multi-line paste line by line, which is exactly why `&&` — not understood by PowerShell 5.1 — and `;` — not understood by `cmd` — are deliberately **not** used as separators). Once a command expires, Copy all copies only the still-pending ones; when everything is granted (or every panel is collapsed) the button disappears by itself. The two challenges have independent TTLs and probe cycles, so the wording only promises "every switch unlocks by itself once they run" and **never the same instant** — the countdown stays per panel, and each panel's copy / regenerate / collapse keeps acting on its own capability only. In-place grants are **persistent**: they live in `<DSH home>/dsh-kit/capability-grants.json` (0600; the confirmation dir is `<DSH home>/dsh-kit/grant-confirm/`, 0700 — kept inside the kit's own subdirectory, because mechanism names dropped into the shared home root can collide with the harness or another plugin), are read at the next boot, and then **take effect with no further confirmation** — so every boot logs one `elevation: load capability=… via=file grantedAt=…` line per loaded grant. Over HTTP they can always be **turned off** (the emergency brake must not depend on a restart), but setting `true` without a grant is rejected with 400 and both routes spelled out. A `true` in the config is **not** a grant (it lives in the same store HTTP writes to, so the source is indistinguishable). **Upgrade note**: switches opened through the UI before this change become off — set the variable and restart, or grant in place. **Why**: neither the loopback fence nor the same-origin proof stops cross-site pages or in-page scripts (they can just set `Sec-Fetch-Site: same-origin`), and the docker socket is root on the target host; details (including whom this does *not* stop) in [architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么) |
|
|
467
541
|
| `execTimeoutSec` | 30 | default exec timeout in seconds (1–120) |
|
|
468
542
|
| `pollIntervalSec` | 5 | stats refresh interval for the panel, in seconds (1–60) |
|
|
469
543
|
| `logTailDefault` | 200 | default log tail line count (1–5000) |
|
|
@@ -520,6 +594,7 @@ and reconnect". The record list can be deleted / reset in the settings card (del
|
|
|
520
594
|
| Tool | Registration | Parameters | Purpose / typical use |
|
|
521
595
|
| --- | --- | --- | --- |
|
|
522
596
|
| `docker_targets` | always registered | `probe?: boolean` | lists targets (name / kind / label); `probe:true` probes the docker version and daemon reachability for each one (SSH targets open connections, so it is slower). Other tools take their `target` from here |
|
|
597
|
+
| `docker_connect_local` | always registered | — (no parameters) | **Connect local**: makes sure a `kind=local` target exists and selects it (an existing one is reused, never duplicated; SSH / custom targets stay untouched), then probes the daemon read-only: returns `{name, kind, created, saved, reachable, serverVersion?}`. When nothing is configured yet `docker_ps` and friends report "no Docker targets configured" — this is the one step that fixes it. **Requires no capability grant** (it only writes the plugin's own config plus a read-only probe) and never installs Docker / starts a daemon / switches context / touches environment variables. When the daemon is unreachable the result text names the cause (CLI missing / probe timed out / daemon not running / no socket permission; when the CLI is missing it also names another container CLI found on PATH, but never rewrites dockerBin for you) instead of an empty list |
|
|
523
598
|
| `docker_ps` | always registered | `target?` (**pass `*` = all targets**), `all?: boolean` | lists containers (name / state / health / image / ports / compose project and service / short ID); by default only running ones, `all:true` includes stopped. With `target:'*'` it returns results grouped by target, and **one unreachable target does not affect the others** (that group carries `error`). Ports are merged across the IPv4/IPv6 dual-stack expansion (one `-p` no longer shows twice, D130); an empty `ports` does **not** mean "nothing exposed" — host-network containers publish on the host itself, and that case now carries a `net` field (D131). The first step of troubleshooting |
|
|
524
599
|
| `docker_attention` | always registered | `target?` (supports `*`), `limit?: number` | a **needs-attention summary**: unhealthy / repeatedly restarting / OOM-killed / non-zero exit / zombie; every item carries `reasons`, `exitCode`, `oomKilled`, `restartCount`. OOM and the real exit code come from one `docker inspect` (a ps summary cannot distinguish a manual kill from 137). The troubleshooting entry point: call it first when you are unsure which machine or container to look at |
|
|
525
600
|
| `docker_inspect` | always registered | `target?`, `id` (required) | `docker inspect`'s authoritative details: state / health check / exit code / restart count / ports / mounts / networks / startup command |
|
|
@@ -540,6 +615,18 @@ and reconnect". The record list can be deleted / reset in the settings card (del
|
|
|
540
615
|
required, and the error message lists the available target names.
|
|
541
616
|
- Changing either switch **re-registers the tools immediately**: after turning off `allowMutations` / `allowExec`
|
|
542
617
|
the corresponding tools (including the three image mutation tools) disappear from the agent side, with no restart.
|
|
618
|
+
- **Registered calls are also subject to the session permission tier gate (tier gate)**: by class — every
|
|
619
|
+
“always registered” tool in the table above is `read` (allowed on every tier); `docker_action` /
|
|
620
|
+
`docker_image_remove` / `docker_image_prune` / `docker_image_pull` are `write`, `docker_exec` is `exec`,
|
|
621
|
+
and these two classes trigger an **approval prompt on every call** under a confined tier (Read Only /
|
|
622
|
+
Workspace Write — the call runs only if the user allows this one; deterministically denied when unattended,
|
|
623
|
+
i.e. approval policy `never`). Full access never prompts, and under Auto review the plugin does not add its
|
|
624
|
+
own prompt (the host’s model pre-review already covers every call). When the host has not composed the
|
|
625
|
+
`sandboxPolicy` / `approval` services the gate is off (behaviour identical to before), and the startup log
|
|
626
|
+
says so: `tier-gate: services absent (…), per-call gate disabled`. This is a **separate layer** from the two
|
|
627
|
+
static switches above: the switches govern registration (host-level, persistent grant), the tier gate governs
|
|
628
|
+
each registered call (per call, follows the tier immediately). Mechanism:
|
|
629
|
+
[docs/permission-tier-plan.md](../../docs/permission-tier-plan.md).
|
|
543
630
|
- Recommended troubleshooting order: `docker_targets` → `docker_ps` → `docker_logs` →
|
|
544
631
|
`docker_inspect` → `docker_stats`; for image problems `docker_images` →
|
|
545
632
|
`docker_image_inspect`.
|
|
@@ -556,8 +643,8 @@ loopback, `localhost`, or a hostname / `/etc/hosts` alias that **resolves to thi
|
|
|
556
643
|
`referrer-policy: no-referrer`.
|
|
557
644
|
|
|
558
645
|
**On top of loopback there is a second gate, the "same-origin proof"**: the four SSE streams (`/logs/stream`,
|
|
559
|
-
`/stats/stream`, `/events/stream`, `/images/pull/stream`) and
|
|
560
|
-
`/images/remove|prune`, `/networks/remove|prune`, `/volumes/remove|prune`, `/exec`) require either
|
|
646
|
+
`/stats/stream`, `/events/stream`, `/images/pull/stream`) and nine write sub-routes (`/action`,
|
|
647
|
+
`/images/remove|prune`, `/networks/remove|prune`, `/volumes/remove|prune`, `/exec`, `/connect-local`) require either
|
|
561
648
|
`Origin: <same origin>` or `Sec-Fetch-Site: same-origin`, otherwise 403 `缺少同源证明` (missing same-origin
|
|
562
649
|
proof). This blocks cross-site side effects such as a malicious page using
|
|
563
650
|
`<img src=.../images/pull/stream>` to trigger a real pull; curl, old Safari and some WebViews do not send those
|
|
@@ -579,6 +666,7 @@ returned 403 on Desktop — the symptom was the logs / stats / events / pull str
|
|
|
579
666
|
| `/config` | GET | — | `{ok:true, config}`: the config snapshot (targets expose only `passwordSet` / `passphraseSet`, plus the read-only `ttyBooks` / `ttyAvailable` / `toolsRegistered`) |
|
|
580
667
|
| `/config` | POST | any subset of the config keys above | `{ok:true, config}`; an unknown key is 400 and invalid JSON is 400 |
|
|
581
668
|
| `/targets` | GET / POST | — | `{ok:true, targets:[{name, kind, label?\|error?}]}` |
|
|
669
|
+
| `/connect-local` | POST | `{}` (the body is ignored) | **Connect local**: reuses or creates a `kind=local` target and returns `{ok:true, result:{name, kind, created, saved, reachable, serverVersion?, message}, config}`; 503 when the host has no settings service and 500 when the save fails (both carry a reason and change no in-memory config). Requires the same-origin proof (it is a write path) but **not** `allowMutations` / `allowExec` |
|
|
582
670
|
| `/probe` | POST | `{target?}` | `{ok:true, probe:{ok, bin, serverVersion, error, target}}` |
|
|
583
671
|
| `/containers` | POST | `{target?, all?}` | `{ok:true, containers: ContainerSummary[]}`; with `target:'*'` it returns `{ok:true, groups:[{target,label,ok,error?,data?}]}` (concurrent cross-target aggregation) |
|
|
584
672
|
| `/attention` | POST | `{target?, limit?}` | for a single target `{ok:true, items: AttentionItem[], total, truncated, degraded}` (`limit` applies **after** filtering + severity sorting; default 100, max 500); with `target:'*'` `{ok:true, groups:[{target,label,ok,error?,data:{items,total,truncated,degraded}}]}` |
|
|
@@ -699,6 +787,11 @@ read-only first:
|
|
|
699
787
|
`id` through the `assertRef` allowlist and clamps parameters the same way. Unlike a snapshot, a long stream has no
|
|
700
788
|
`maxOutputKb` cap (following would lose its point if truncated), and memory protection is shouldered by the
|
|
701
789
|
client's 5000-line ring buffer and 2000-line colouring cap.
|
|
790
|
+
9. **Mutating operations get a per-use audit trail**. The five audit lines from kit elevation answer "what was
|
|
791
|
+
authorized"; this answers "what it was used for": every mutation / exec that **passes the capability gate** lands
|
|
792
|
+
one structured `[dsh-docker] capability-use: …` audit line (outlet: the host's stdout), covering both the panel
|
|
793
|
+
routes and the agent tools. The line format, what is *not* logged (403 / tier-gate asks), and the cost that exec
|
|
794
|
+
commands appear alongside the lines are described in the usage-audit entry under "Known limitations".
|
|
702
795
|
|
|
703
796
|
## Known limitations
|
|
704
797
|
|
|
@@ -793,13 +886,31 @@ read-only first:
|
|
|
793
886
|
withdrawn, and data routes other than `/config` return 403 (long streams in progress — logs / stats / pulling —
|
|
794
887
|
are wrapped up immediately too). The route objects themselves are not unloaded; they are blocked by the 403, and
|
|
795
888
|
`/config` stays readable and writable — the settings card is the way back in, with no restart of `dsh web` needed.
|
|
796
|
-
- **
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
with
|
|
889
|
+
- **The usage audit goes to the host's stdout, and exec commands appear in it**: every mutation / exec that
|
|
890
|
+
**passes the capability gate** (the panel's mutation / exec routes and the agent tools `docker_action` /
|
|
891
|
+
`docker_image_remove` / `docker_image_prune` / `docker_image_pull` / `docker_exec`; 403 rejections and parameter
|
|
892
|
+
validation 400s are not logged — "a use = something that passed the gate") lands one
|
|
893
|
+
`[dsh-docker] capability-use: capability=allowMutations|allowExec source=http|tool action=container.remove|…
|
|
894
|
+
target=… ref=… ok=true|false durationMs=…` line in the same key=value style as kit elevation's authorization
|
|
895
|
+
lines, with `capability` matching those lines directly; the pull progress stream logs a start / end pair (if the
|
|
896
|
+
host dies mid-stream, at least the start line remains). **The outlet is `console.log`** (docker D162: on a real
|
|
897
|
+
host, `ctx.logger` lines were measured to reach neither the host's stdout nor any file, and an audit line whose
|
|
898
|
+
whole point is "findable when something goes wrong" is worthless in an invisible channel; tier-gate's log outlet
|
|
899
|
+
makes the same call) — the terminal running `dsh web` shows them, grep works. Kit elevation's authorization lines share the same stdout outlet
|
|
900
|
+
(kit D14 — both line families on one channel, searchable side by side). **The cost**: the exec command is recorded in the
|
|
901
|
+
trailing `detail` field (together with the exit code, e.g. `detail=code=0 cmd=ls -la /app`), truncated to 200
|
|
902
|
+
characters overall with control characters escaped as `\n` literals — a line meant for humans to read has to
|
|
903
|
+
accept appearing in the terminal-visible log stream, the same trade-off in the positive direction as elevation's
|
|
904
|
+
"nonce never enters the log". A docker error also counts as a use (`ok=false` plus `detail=` with the truncated
|
|
905
|
+
error text); tier-gate asks / denies are not logged (nothing was executed, and the host's approval log already
|
|
906
|
+
covers them). There is no query UI and no separate audit file — the way to search it is the host's stdout.
|
|
907
|
+
|
|
908
|
+
- **The boundaries of cross-target aggregation**: concurrency cap 4; a **90s per-target budget (fallback)**, while
|
|
909
|
+
each command's own timeout (30s) is the one actually honoured — when a budget fires, the underlying child process /
|
|
910
|
+
SSH channel is **really cancelled** (not merely reported as a failed cell), otherwise the timed-out command keeps
|
|
911
|
+
holding a remote session slot (D161). A single target failing or timing out affects only its own cell (the group
|
|
912
|
+
carries `error`). With many targets the Overview's request volume grows linearly with the target count (2 requests
|
|
913
|
+
per target), and auto refresh multiplies that volume — with many targets it is best to turn auto refresh off.
|
|
803
914
|
|
|
804
915
|
## How it works
|
|
805
916
|
|
|
@@ -889,7 +1000,7 @@ injection)**, `assertBin`, `formatBytes`, `shJoin` escaping, `DockerApi`'s argv
|
|
|
889
1000
|
|
|
890
1001
|
`scripts/route-smoke.mjs` runs end to end with **a fake cordis ctx + a fake docker CLI script**: plugin
|
|
891
1002
|
mounting (settings / tools / routes / capability-announcement registration), the actual calls and returns of
|
|
892
|
-
**
|
|
1003
|
+
**27 routes** (including the event sequences and parameter validation of the four SSE streams `/logs/stream`,
|
|
893
1004
|
`/stats/stream`, `/events/stream`, `/images/pull/stream`, with the event stream additionally asserting that noise is
|
|
894
1005
|
dropped by the allowlist), the `docker_events` tool's snapshot output and its `since` character-set validation,
|
|
895
1006
|
`/images/inspect`'s details + build history, `/config` credential masking, 400 for unknown config keys, 403 for
|
|
@@ -977,15 +1088,30 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
977
1088
|
details / logs work; the first connection logs "host key fingerprint recorded (TOFU)" and the second does not
|
|
978
1089
|
prompt again; after manually changing the fingerprint in `hostKeys` and reconnecting, the connection should
|
|
979
1090
|
**be rejected** with reset guidance.
|
|
980
|
-
3. **
|
|
981
|
-
|
|
982
|
-
|
|
1091
|
+
3. **Connect local (one click)** — exercise **both** entries:
|
|
1092
|
+
- **With no targets**: clear every target and save (or use a fresh profile), open the panel — the
|
|
1093
|
+
empty state in the body shows a "Connect local" primary button, and the dropdown should **not**
|
|
1094
|
+
list it again; one click should produce a `kind=local` target named `local`, select it
|
|
1095
|
+
immediately, and load the container list;
|
|
1096
|
+
- **With other targets**: keep a few SSH hosts and open the target dropdown — the last item is
|
|
1097
|
+
`+ Connect local`, and the toolbar must carry **no** permanent entry; picking it should likewise
|
|
1098
|
+
add `local` and switch to it, after which the item disappears from the dropdown (a local target
|
|
1099
|
+
now exists).
|
|
1100
|
+
Neither place may show "the same entry listed twice"; nothing should install Docker, start a daemon
|
|
1101
|
+
or switch contexts along the way. On a host without docker you should see a named cause (CLI
|
|
1102
|
+
missing / probe timed out / daemon not running / no socket permission) rather than an empty list.
|
|
1103
|
+
- **On a machine that only has podman** (drop docker from `PATH`, leave podman): that sentence
|
|
1104
|
+
should **name podman** and say "put podman in 'docker CLI' and it works", while **not**
|
|
1105
|
+
rewriting `dockerBin` for you (go back to the settings card — the value is still `docker`).
|
|
1106
|
+
4. **Read-only interception**: with both switches off, `/action`, `/exec`, `/images/remove`,
|
|
1107
|
+
`/images/prune`, `/images/pull/stream` all return 403; on the agent side exactly 12 read-only tools are
|
|
1108
|
+
registered (`docker_targets` / `connect_local` / `ps` / `attention` / `inspect` / `logs` / `stats` / `events` / `images` /
|
|
983
1109
|
`image_inspect` / `networks` / `volumes` — see the tool table for the authoritative list), and
|
|
984
1110
|
the panel's start / stop / remove, image removal, pruning and pull buttons are greyed out. After turning on
|
|
985
1111
|
"allow mutations" these routes and tools appear immediately (no restart needed).
|
|
986
|
-
|
|
1112
|
+
5. **Logs / stats / images**: `tail` and `timestamps` / `since` take effect; stats show
|
|
987
1113
|
CPU, memory, network and block IO; the image list carries a marker on dangling entries.
|
|
988
|
-
|
|
1114
|
+
6. **FOLLOW live log stream**: turning on `FOLLOW` on the log page → the status line first says
|
|
989
1115
|
"connecting" and then "following live", and new lines from `docker logs -f` appear immediately (`docker run --rm alpine sh
|
|
990
1116
|
-c 'i=0; while :; do echo line-$i; i=$((i+1)); sleep 1; done'` makes this observable);
|
|
991
1117
|
with FOLLOW on, `AUTO REFRESH` is greyed out and polling stops; scrolling up brings up "back to bottom",
|
|
@@ -996,26 +1122,26 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
996
1122
|
itself, with no error banner. Run the same with an SSH target and confirm that panel operations such as
|
|
997
1123
|
`docker ps` on the same host are unaffected while the stream runs (connection reuse), and that idle reclamation
|
|
998
1124
|
(120s) does not cut the stream.
|
|
999
|
-
|
|
1125
|
+
7. **Stats live following**: turning on `FOLLOW` on the stats page → "connecting to the stats stream…" → "following
|
|
1000
1126
|
live (docker stats)", and the CPU / memory sparklines grow a little every second (run `docker run --rm
|
|
1001
1127
|
alpine sh -c 'while :; do :; done'` to watch CPU rise); turning FOLLOW off immediately returns to snapshots and
|
|
1002
1128
|
resumes polling. Stop the containers being measured → the stream receives `end` (stats-exit), hints, and then
|
|
1003
1129
|
returns to polling automatically.
|
|
1004
1130
|
Note that **this stream does not end naturally**: switching pages / closing the panel must close the `EventSource`
|
|
1005
1131
|
(on the host side `docker stats` should be seen being SIGTERM'd).
|
|
1006
|
-
|
|
1132
|
+
8. **Image details / removal / pruning**: click "details" on a row of the images page → the layer count in the
|
|
1007
1133
|
overview matches `docker image inspect` and the build history matches `docker history` (Docker ≥ 26
|
|
1008
1134
|
goes through `--format`, older versions fall back to the plain-text table); look up a dangling row by image ID.
|
|
1009
1135
|
Click "remove" → after a second confirmation it runs `docker image rm`; removing an image that a container
|
|
1010
1136
|
references should fail with a hint.
|
|
1011
1137
|
"prune dangling" only removes untagged images, and its output ends with `Total reclaimed space`.
|
|
1012
|
-
|
|
1138
|
+
9. **Pull progress stream**: the pull icon in the images toolbar (hovering shows "pull image") → enter a small image
|
|
1013
1139
|
you do not have locally (such as `alpine:3.20`)
|
|
1014
1140
|
→ per-layer status lines appear live and are updated in place by layer; when it finishes it hints "pull complete"
|
|
1015
1141
|
and refreshes the list automatically.
|
|
1016
1142
|
Clicking "stop" mid-pull or leaving the view → `docker pull` is terminated by SIGTERM with no leftover process.
|
|
1017
1143
|
With "allow mutations" off the button is greyed out, and going straight to `/images/pull/stream` returns 403.
|
|
1018
|
-
|
|
1144
|
+
10. **Compose project view**: start two or three services with a compose file (`docker compose up -d`) →
|
|
1019
1145
|
switch the toolbar to "Compose" → the services are grouped under one project card with the correct
|
|
1020
1146
|
running / service counts and states;
|
|
1021
1147
|
click into the project to see the service table, switch to "merged logs" → the services' logs appear mixed by the
|
|
@@ -1023,12 +1149,12 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1023
1149
|
content; after turning "auto
|
|
1024
1150
|
scroll" off new logs keep entering the buffer without the view jumping. Containers without a compose label are
|
|
1025
1151
|
grouped under "other containers (non-compose)".
|
|
1026
|
-
|
|
1152
|
+
11. **After turning on `allowMutations`**: stop / start / restart succeed; removing a running container
|
|
1027
1153
|
errors with a "stop it before removing" hint, and stopping first and then removing succeeds.
|
|
1028
|
-
|
|
1154
|
+
12. **After turning on `allowExec`**: a command such as `ls -la /app` returns stdout and the exit code; changing the
|
|
1029
1155
|
command to `sleep 60` (with `timeoutSec` lowered) should be interrupted and report a timeout; a `command` longer
|
|
1030
1156
|
than 8000 characters is rejected.
|
|
1031
|
-
|
|
1157
|
+
13. **The exec drawer coexists with the body** (tty ≥ 0.15): after opening the drawer from a card's "Terminal", switch
|
|
1032
1158
|
to logs / stats / another container's details, and both the drawer and the terminal session must stay alive;
|
|
1033
1159
|
**collapsing** (the arrow or double-clicking the top edge) squashes the drawer into one title bar and the session
|
|
1034
1160
|
keeps running (expanding restores it); **dragging the top edge** changes the height without exceeding 75% of the
|
|
@@ -1037,7 +1163,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1037
1163
|
Offline regression: `node packages/tty/scripts/preview.mjs docker-exec-logs`
|
|
1038
1164
|
(headless Chrome runs the real client: drawer + log page + collapse and expand, asserting that the session was
|
|
1039
1165
|
not ended).
|
|
1040
|
-
|
|
1166
|
+
14. **Entering the container panel from the connection bar (dock mode, needs tty ≥ 0.16)**: open an SSH tab in the
|
|
1041
1167
|
tty panel →
|
|
1042
1168
|
connection bar "Containers" → the container panel should dock **to the right of the terminal** (not a full-screen
|
|
1043
1169
|
modal) while the terminal stays typable;
|
|
@@ -1047,7 +1173,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1047
1173
|
`<container> · exec` tab in the same terminal panel** rather than nesting another terminal drawer.
|
|
1048
1174
|
Offline regression: `node packages/tty/scripts/preview.mjs docker-dock`
|
|
1049
1175
|
(asserting docked / no backdrop / the terminal widens after collapsing / the container panel survives).
|
|
1050
|
-
|
|
1176
|
+
15. **Multi-select merged logs**: click `Select for merging` in the container list → checkboxes appear on the left of
|
|
1051
1177
|
the cards and the card action bars collapse;
|
|
1052
1178
|
check 2–3 containers → the action bar shows "3 containers selected", and clicking `merged logs` → the merged
|
|
1053
1179
|
view's title is
|
|
@@ -1055,11 +1181,11 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1055
1181
|
checked the button is greyed out and hints "select at least 2 containers", and at 9 checked it is greyed out and
|
|
1056
1182
|
hints at a maximum of 8; stop one of the containers from the list → that stream ends with the usual `end`
|
|
1057
1183
|
semantics while the others are unaffected.
|
|
1058
|
-
|
|
1184
|
+
16. **Esc leaves selection mode**: pressing Esc in selection mode → back to the normal list, checks cleared and the
|
|
1059
1185
|
action bar gone;
|
|
1060
1186
|
clicking `Select for merging` again (its label is now `Exit selection`) has the same effect; switching targets /
|
|
1061
1187
|
switching segments / closing the panel also clear the selection mode and the checks together.
|
|
1062
|
-
|
|
1188
|
+
17. **The Activity strip**: the "Activity" strip appears at the head of the container list, its status dot goes from
|
|
1063
1189
|
yellow to green and the copy reads "receiving live
|
|
1064
1190
|
(docker events)"; running `docker restart <container>` / `docker stop`+`start` → the
|
|
1065
1191
|
list state follows within 1 second and the Activity strip shows `stop` / `start` (abnormal exits show the coded
|
|
@@ -1069,7 +1195,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1069
1195
|
server-side allowlist);
|
|
1070
1196
|
after the network drops / `dsh web` restarts, recovery briefly shows a yellow status dot and automatically does
|
|
1071
1197
|
one full list refresh.
|
|
1072
|
-
|
|
1198
|
+
18. **Networks / volumes**: the toolbar segments show "Networks" and "Volumes". The network list should contain
|
|
1073
1199
|
`bridge` / `host` / `none`
|
|
1074
1200
|
plus project networks created by compose; click one to enter details → the subnet / gateway in the overview match
|
|
1075
1201
|
`docker network
|
|
@@ -1082,7 +1208,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1082
1208
|
volume pruning raises a confirmation saying "the data will be deleted as well". **Note**: on docker < 23 volume
|
|
1083
1209
|
pruning also deletes named volumes, so it is best to confirm on a test target first.
|
|
1084
1210
|
|
|
1085
|
-
|
|
1211
|
+
19. **Multi-target Overview**: configure two or more targets (local + one SSH) → the `Overview` pill appears next to
|
|
1086
1212
|
the target picker;
|
|
1087
1213
|
clicking it shows two counter cards on one screen (each badged "local" / "SSH") plus the attention area, with the
|
|
1088
1214
|
card numbers matching that target's
|
|
@@ -1096,7 +1222,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1096
1222
|
"all good"; while targets are still answering it shows "reading…". Turning on "auto refresh" → polls all targets
|
|
1097
1223
|
at
|
|
1098
1224
|
`pollIntervalSec` (turning it off stops that).
|
|
1099
|
-
|
|
1225
|
+
20. **Switching targets does not cross-talk (the list write gate)**: let Target1 (an unreachable SSH host) fail first
|
|
1100
1226
|
and raise an "operation failed" banner,
|
|
1101
1227
|
then immediately switch to the healthy Target2 → the banner should **disappear with the switch at once** and the
|
|
1102
1228
|
list should be Target2's containers; then wait 20s
|