@hyzyn/dsh-docker 0.9.5 → 0.9.6
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 +120 -27
- package/README.md +106 -26
- package/client-src/docker.css +326 -160
- package/client-src/index.js +288 -21
- package/client.js +349 -183
- 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 +49 -0
- package/lib/index.js +390 -10
- 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 +1 -1
- package/scripts/client-smoke.mjs +205 -0
- package/scripts/log-perf.mjs +9 -2
- package/scripts/route-smoke.mjs +73 -4
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,7 +534,7 @@ 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
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). 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-一条请求经过什么) |
|
|
@@ -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 |
|
|
@@ -556,8 +631,8 @@ loopback, `localhost`, or a hostname / `/etc/hosts` alias that **resolves to thi
|
|
|
556
631
|
`referrer-policy: no-referrer`.
|
|
557
632
|
|
|
558
633
|
**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
|
|
634
|
+
`/stats/stream`, `/events/stream`, `/images/pull/stream`) and nine write sub-routes (`/action`,
|
|
635
|
+
`/images/remove|prune`, `/networks/remove|prune`, `/volumes/remove|prune`, `/exec`, `/connect-local`) require either
|
|
561
636
|
`Origin: <same origin>` or `Sec-Fetch-Site: same-origin`, otherwise 403 `缺少同源证明` (missing same-origin
|
|
562
637
|
proof). This blocks cross-site side effects such as a malicious page using
|
|
563
638
|
`<img src=.../images/pull/stream>` to trigger a real pull; curl, old Safari and some WebViews do not send those
|
|
@@ -579,6 +654,7 @@ returned 403 on Desktop — the symptom was the logs / stats / events / pull str
|
|
|
579
654
|
| `/config` | GET | — | `{ok:true, config}`: the config snapshot (targets expose only `passwordSet` / `passphraseSet`, plus the read-only `ttyBooks` / `ttyAvailable` / `toolsRegistered`) |
|
|
580
655
|
| `/config` | POST | any subset of the config keys above | `{ok:true, config}`; an unknown key is 400 and invalid JSON is 400 |
|
|
581
656
|
| `/targets` | GET / POST | — | `{ok:true, targets:[{name, kind, label?\|error?}]}` |
|
|
657
|
+
| `/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
658
|
| `/probe` | POST | `{target?}` | `{ok:true, probe:{ok, bin, serverVersion, error, target}}` |
|
|
583
659
|
| `/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
660
|
| `/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}}]}` |
|
|
@@ -796,10 +872,12 @@ read-only first:
|
|
|
796
872
|
- **Mutating operations have no separate audit log**: only docker's own records and the host `ctx.logger`'s
|
|
797
873
|
ordinary output.
|
|
798
874
|
|
|
799
|
-
- **The boundaries of cross-target aggregation**: concurrency cap 4
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
875
|
+
- **The boundaries of cross-target aggregation**: concurrency cap 4; a **90s per-target budget (fallback)**, while
|
|
876
|
+
each command's own timeout (30s) is the one actually honoured — when a budget fires, the underlying child process /
|
|
877
|
+
SSH channel is **really cancelled** (not merely reported as a failed cell), otherwise the timed-out command keeps
|
|
878
|
+
holding a remote session slot (D161). A single target failing or timing out affects only its own cell (the group
|
|
879
|
+
carries `error`). With many targets the Overview's request volume grows linearly with the target count (2 requests
|
|
880
|
+
per target), and auto refresh multiplies that volume — with many targets it is best to turn auto refresh off.
|
|
803
881
|
|
|
804
882
|
## How it works
|
|
805
883
|
|
|
@@ -889,7 +967,7 @@ injection)**, `assertBin`, `formatBytes`, `shJoin` escaping, `DockerApi`'s argv
|
|
|
889
967
|
|
|
890
968
|
`scripts/route-smoke.mjs` runs end to end with **a fake cordis ctx + a fake docker CLI script**: plugin
|
|
891
969
|
mounting (settings / tools / routes / capability-announcement registration), the actual calls and returns of
|
|
892
|
-
**
|
|
970
|
+
**27 routes** (including the event sequences and parameter validation of the four SSE streams `/logs/stream`,
|
|
893
971
|
`/stats/stream`, `/events/stream`, `/images/pull/stream`, with the event stream additionally asserting that noise is
|
|
894
972
|
dropped by the allowlist), the `docker_events` tool's snapshot output and its `since` character-set validation,
|
|
895
973
|
`/images/inspect`'s details + build history, `/config` credential masking, 400 for unknown config keys, 403 for
|
|
@@ -977,15 +1055,30 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
977
1055
|
details / logs work; the first connection logs "host key fingerprint recorded (TOFU)" and the second does not
|
|
978
1056
|
prompt again; after manually changing the fingerprint in `hostKeys` and reconnecting, the connection should
|
|
979
1057
|
**be rejected** with reset guidance.
|
|
980
|
-
3. **
|
|
981
|
-
|
|
982
|
-
|
|
1058
|
+
3. **Connect local (one click)** — exercise **both** entries:
|
|
1059
|
+
- **With no targets**: clear every target and save (or use a fresh profile), open the panel — the
|
|
1060
|
+
empty state in the body shows a "Connect local" primary button, and the dropdown should **not**
|
|
1061
|
+
list it again; one click should produce a `kind=local` target named `local`, select it
|
|
1062
|
+
immediately, and load the container list;
|
|
1063
|
+
- **With other targets**: keep a few SSH hosts and open the target dropdown — the last item is
|
|
1064
|
+
`+ Connect local`, and the toolbar must carry **no** permanent entry; picking it should likewise
|
|
1065
|
+
add `local` and switch to it, after which the item disappears from the dropdown (a local target
|
|
1066
|
+
now exists).
|
|
1067
|
+
Neither place may show "the same entry listed twice"; nothing should install Docker, start a daemon
|
|
1068
|
+
or switch contexts along the way. On a host without docker you should see a named cause (CLI
|
|
1069
|
+
missing / probe timed out / daemon not running / no socket permission) rather than an empty list.
|
|
1070
|
+
- **On a machine that only has podman** (drop docker from `PATH`, leave podman): that sentence
|
|
1071
|
+
should **name podman** and say "put podman in 'docker CLI' and it works", while **not**
|
|
1072
|
+
rewriting `dockerBin` for you (go back to the settings card — the value is still `docker`).
|
|
1073
|
+
4. **Read-only interception**: with both switches off, `/action`, `/exec`, `/images/remove`,
|
|
1074
|
+
`/images/prune`, `/images/pull/stream` all return 403; on the agent side exactly 12 read-only tools are
|
|
1075
|
+
registered (`docker_targets` / `connect_local` / `ps` / `attention` / `inspect` / `logs` / `stats` / `events` / `images` /
|
|
983
1076
|
`image_inspect` / `networks` / `volumes` — see the tool table for the authoritative list), and
|
|
984
1077
|
the panel's start / stop / remove, image removal, pruning and pull buttons are greyed out. After turning on
|
|
985
1078
|
"allow mutations" these routes and tools appear immediately (no restart needed).
|
|
986
|
-
|
|
1079
|
+
5. **Logs / stats / images**: `tail` and `timestamps` / `since` take effect; stats show
|
|
987
1080
|
CPU, memory, network and block IO; the image list carries a marker on dangling entries.
|
|
988
|
-
|
|
1081
|
+
6. **FOLLOW live log stream**: turning on `FOLLOW` on the log page → the status line first says
|
|
989
1082
|
"connecting" and then "following live", and new lines from `docker logs -f` appear immediately (`docker run --rm alpine sh
|
|
990
1083
|
-c 'i=0; while :; do echo line-$i; i=$((i+1)); sleep 1; done'` makes this observable);
|
|
991
1084
|
with FOLLOW on, `AUTO REFRESH` is greyed out and polling stops; scrolling up brings up "back to bottom",
|
|
@@ -996,26 +1089,26 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
996
1089
|
itself, with no error banner. Run the same with an SSH target and confirm that panel operations such as
|
|
997
1090
|
`docker ps` on the same host are unaffected while the stream runs (connection reuse), and that idle reclamation
|
|
998
1091
|
(120s) does not cut the stream.
|
|
999
|
-
|
|
1092
|
+
7. **Stats live following**: turning on `FOLLOW` on the stats page → "connecting to the stats stream…" → "following
|
|
1000
1093
|
live (docker stats)", and the CPU / memory sparklines grow a little every second (run `docker run --rm
|
|
1001
1094
|
alpine sh -c 'while :; do :; done'` to watch CPU rise); turning FOLLOW off immediately returns to snapshots and
|
|
1002
1095
|
resumes polling. Stop the containers being measured → the stream receives `end` (stats-exit), hints, and then
|
|
1003
1096
|
returns to polling automatically.
|
|
1004
1097
|
Note that **this stream does not end naturally**: switching pages / closing the panel must close the `EventSource`
|
|
1005
1098
|
(on the host side `docker stats` should be seen being SIGTERM'd).
|
|
1006
|
-
|
|
1099
|
+
8. **Image details / removal / pruning**: click "details" on a row of the images page → the layer count in the
|
|
1007
1100
|
overview matches `docker image inspect` and the build history matches `docker history` (Docker ≥ 26
|
|
1008
1101
|
goes through `--format`, older versions fall back to the plain-text table); look up a dangling row by image ID.
|
|
1009
1102
|
Click "remove" → after a second confirmation it runs `docker image rm`; removing an image that a container
|
|
1010
1103
|
references should fail with a hint.
|
|
1011
1104
|
"prune dangling" only removes untagged images, and its output ends with `Total reclaimed space`.
|
|
1012
|
-
|
|
1105
|
+
9. **Pull progress stream**: the pull icon in the images toolbar (hovering shows "pull image") → enter a small image
|
|
1013
1106
|
you do not have locally (such as `alpine:3.20`)
|
|
1014
1107
|
→ per-layer status lines appear live and are updated in place by layer; when it finishes it hints "pull complete"
|
|
1015
1108
|
and refreshes the list automatically.
|
|
1016
1109
|
Clicking "stop" mid-pull or leaving the view → `docker pull` is terminated by SIGTERM with no leftover process.
|
|
1017
1110
|
With "allow mutations" off the button is greyed out, and going straight to `/images/pull/stream` returns 403.
|
|
1018
|
-
|
|
1111
|
+
10. **Compose project view**: start two or three services with a compose file (`docker compose up -d`) →
|
|
1019
1112
|
switch the toolbar to "Compose" → the services are grouped under one project card with the correct
|
|
1020
1113
|
running / service counts and states;
|
|
1021
1114
|
click into the project to see the service table, switch to "merged logs" → the services' logs appear mixed by the
|
|
@@ -1023,12 +1116,12 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1023
1116
|
content; after turning "auto
|
|
1024
1117
|
scroll" off new logs keep entering the buffer without the view jumping. Containers without a compose label are
|
|
1025
1118
|
grouped under "other containers (non-compose)".
|
|
1026
|
-
|
|
1119
|
+
11. **After turning on `allowMutations`**: stop / start / restart succeed; removing a running container
|
|
1027
1120
|
errors with a "stop it before removing" hint, and stopping first and then removing succeeds.
|
|
1028
|
-
|
|
1121
|
+
12. **After turning on `allowExec`**: a command such as `ls -la /app` returns stdout and the exit code; changing the
|
|
1029
1122
|
command to `sleep 60` (with `timeoutSec` lowered) should be interrupted and report a timeout; a `command` longer
|
|
1030
1123
|
than 8000 characters is rejected.
|
|
1031
|
-
|
|
1124
|
+
13. **The exec drawer coexists with the body** (tty ≥ 0.15): after opening the drawer from a card's "Terminal", switch
|
|
1032
1125
|
to logs / stats / another container's details, and both the drawer and the terminal session must stay alive;
|
|
1033
1126
|
**collapsing** (the arrow or double-clicking the top edge) squashes the drawer into one title bar and the session
|
|
1034
1127
|
keeps running (expanding restores it); **dragging the top edge** changes the height without exceeding 75% of the
|
|
@@ -1037,7 +1130,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1037
1130
|
Offline regression: `node packages/tty/scripts/preview.mjs docker-exec-logs`
|
|
1038
1131
|
(headless Chrome runs the real client: drawer + log page + collapse and expand, asserting that the session was
|
|
1039
1132
|
not ended).
|
|
1040
|
-
|
|
1133
|
+
14. **Entering the container panel from the connection bar (dock mode, needs tty ≥ 0.16)**: open an SSH tab in the
|
|
1041
1134
|
tty panel →
|
|
1042
1135
|
connection bar "Containers" → the container panel should dock **to the right of the terminal** (not a full-screen
|
|
1043
1136
|
modal) while the terminal stays typable;
|
|
@@ -1047,7 +1140,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1047
1140
|
`<container> · exec` tab in the same terminal panel** rather than nesting another terminal drawer.
|
|
1048
1141
|
Offline regression: `node packages/tty/scripts/preview.mjs docker-dock`
|
|
1049
1142
|
(asserting docked / no backdrop / the terminal widens after collapsing / the container panel survives).
|
|
1050
|
-
|
|
1143
|
+
15. **Multi-select merged logs**: click `Select for merging` in the container list → checkboxes appear on the left of
|
|
1051
1144
|
the cards and the card action bars collapse;
|
|
1052
1145
|
check 2–3 containers → the action bar shows "3 containers selected", and clicking `merged logs` → the merged
|
|
1053
1146
|
view's title is
|
|
@@ -1055,11 +1148,11 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1055
1148
|
checked the button is greyed out and hints "select at least 2 containers", and at 9 checked it is greyed out and
|
|
1056
1149
|
hints at a maximum of 8; stop one of the containers from the list → that stream ends with the usual `end`
|
|
1057
1150
|
semantics while the others are unaffected.
|
|
1058
|
-
|
|
1151
|
+
16. **Esc leaves selection mode**: pressing Esc in selection mode → back to the normal list, checks cleared and the
|
|
1059
1152
|
action bar gone;
|
|
1060
1153
|
clicking `Select for merging` again (its label is now `Exit selection`) has the same effect; switching targets /
|
|
1061
1154
|
switching segments / closing the panel also clear the selection mode and the checks together.
|
|
1062
|
-
|
|
1155
|
+
17. **The Activity strip**: the "Activity" strip appears at the head of the container list, its status dot goes from
|
|
1063
1156
|
yellow to green and the copy reads "receiving live
|
|
1064
1157
|
(docker events)"; running `docker restart <container>` / `docker stop`+`start` → the
|
|
1065
1158
|
list state follows within 1 second and the Activity strip shows `stop` / `start` (abnormal exits show the coded
|
|
@@ -1069,7 +1162,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1069
1162
|
server-side allowlist);
|
|
1070
1163
|
after the network drops / `dsh web` restarts, recovery briefly shows a yellow status dot and automatically does
|
|
1071
1164
|
one full list refresh.
|
|
1072
|
-
|
|
1165
|
+
18. **Networks / volumes**: the toolbar segments show "Networks" and "Volumes". The network list should contain
|
|
1073
1166
|
`bridge` / `host` / `none`
|
|
1074
1167
|
plus project networks created by compose; click one to enter details → the subnet / gateway in the overview match
|
|
1075
1168
|
`docker network
|
|
@@ -1082,7 +1175,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1082
1175
|
volume pruning raises a confirmation saying "the data will be deleted as well". **Note**: on docker < 23 volume
|
|
1083
1176
|
pruning also deletes named volumes, so it is best to confirm on a test target first.
|
|
1084
1177
|
|
|
1085
|
-
|
|
1178
|
+
19. **Multi-target Overview**: configure two or more targets (local + one SSH) → the `Overview` pill appears next to
|
|
1086
1179
|
the target picker;
|
|
1087
1180
|
clicking it shows two counter cards on one screen (each badged "local" / "SSH") plus the attention area, with the
|
|
1088
1181
|
card numbers matching that target's
|
|
@@ -1096,7 +1189,7 @@ lines), and the gating of the eight `/networks` and `/volumes` endpoints (403 fo
|
|
|
1096
1189
|
"all good"; while targets are still answering it shows "reading…". Turning on "auto refresh" → polls all targets
|
|
1097
1190
|
at
|
|
1098
1191
|
`pollIntervalSec` (turning it off stops that).
|
|
1099
|
-
|
|
1192
|
+
20. **Switching targets does not cross-talk (the list write gate)**: let Target1 (an unreachable SSH host) fail first
|
|
1100
1193
|
and raise an "operation failed" banner,
|
|
1101
1194
|
then immediately switch to the healthy Target2 → the banner should **disappear with the switch at once** and the
|
|
1102
1195
|
list should be Target2's containers; then wait 20s
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
- **常驻会话右侧栏**:面板作为会话右侧栏标签(`sidebar.right.pane.tab`)与对话**同屏**——发完错误交给 Agent 后日志留在右边,不影响看它干活;折叠即退出视野、不占屏。面板级状态(目标 / 视图 / 过滤词 / 选中容器及它的页签)**跨会话保留**,而折叠会主动断流、把 SSH 通道还回去。拿不到右侧栏服务的老宿主自动退回原有的 dock / 模态,行为与旧版一致。
|
|
10
10
|
- **日志右键交给 Agent**:日志页拖选报错行 → 右键「直接发送到当前会话 / 填入输入框,我先改改」,把「选中的行 + 目标 / 容器 / 时间窗 + 前后各 20 行上下文」交给会话(内容会进模型上下文,菜单里明写了留意凭证)。投递成功有两处回执:**视口级 toast**(挂 `body`,盖过面板与终端弹窗),以及——终端面板正开着时——**自动折起终端**(`ttyPanel` 契约 v2 的 `minimize()`;会话继续跑,恢复靠侧边栏「终端」入口的徽标),让会话直接露出来。老版本 tty 没有这个能力时退化成 toast 里「会话在面板后面」的指引。需要改稿就先「填入输入框」——**编辑面只有会话输入框一个**(它多行、能看到完整上下文,Agent 收到的就是它),不再另开一张更弱的卡片编辑器。目标会话的定位跨宿主版本:≤0.1.5 读 `sessions.list` 快照的 `current`,**0.1.6 起**该字段随「视图选中项」一起搬出了会话域,改看 `retainedBy.mainView > 0`(官方 `dsh-client-ui-session` 与本地 codegraph 的同一判据)——只认老字段会让菜单判成「当前没有打开的会话」而整组置灰;同一份取值也驱动「切会话把容器标签带过去」。
|
|
11
|
+
- **显式一键连接本机**:两个**非常驻**入口——一个目标都没有时是「还没有配置 Docker 目标」空态里的主按钮,已有别的目标时是**目标下拉的末一项** `+ 连接本机`(工具条上**不挂**:常驻时它既占位又招人点,纯噪音)——一次点击(或选中该项)就把宿主所在机器加成本机目标并选中(已有本机目标则直接复用,不重复创建;SSH / 自定义目标原样保留),agent 侧同一条操作是 `docker_connect_local`。它**不**改空 `targets` 的语义、**不**凭空造隐式默认目标、**不**自动装 Docker / 起 daemon / 切 docker context,也不需要任何能力授权(只加一条指向插件自身的本机目标)。
|
|
11
12
|
- **多目标聚合取数**:总览页对全部 `targets[]` 并行请求,单个目标不可达只污染自己那一格;agent 侧同一口径由 `docker_ps target:"*"` / `docker_attention target:"*"` 暴露,跨目标不互相阻塞。
|
|
12
13
|
- **「需关注」读权威字段**:不健康 / 反复重启 / OOM 被杀 / 非零退出 / 僵死;OOM 与真实退出码由一次 `docker inspect` 补齐——`docker ps` 摘要里的 137 分不出 OOM 与手动 kill,只按摘要筛必然误报。
|
|
13
14
|
- **四条 SSE 长流共用一套基建**:日志 FOLLOW、`docker stats`、`docker events`、`docker pull` 走同一个 `openSseStream`(心跳 / 活跃流登记 / 断开清理),差异只在收尾语义——日志与拉取自然结束,统计与事件由前端主动断。多选聚合日志按 `--timestamps` 前缀还原跨容器真实时序,「暂停」只冻结渲染(流继续接收,恢复时一次性补齐)。
|
|
@@ -172,6 +173,13 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
172
173
|
health_status / destroy / rename / update):`exec_*`、`archive-path`(`docker cp`)
|
|
173
174
|
这类噪音在服务端就丢掉了——实测一台跑批机器 24 小时 47 条事件全是 exec,白名单
|
|
174
175
|
命中 0,所以只被 exec 的机器上活动条是空的,这是刻意的。
|
|
176
|
+
断开后**要手动重连**:这条流进了 `closed` 就是终止态(EventSource 只在连接层抖动
|
|
177
|
+
时自愈;`docker events` 进程退出、服务端发 `end` 帧都会走 close),活动条上会出现
|
|
178
|
+
一个 **重连** 按钮(只在 `closed` 时出现;`unsupported` 环境不给——重连必然再失败),
|
|
179
|
+
工具条的 ⟳ 也一并重连。**AUTO REFRESH 轮询刻意不重连**:塞进轮询会让一条已终止的流
|
|
180
|
+
被不断重开,既掩盖「它断了」,又白建 SSE。重连成功后补一次全量列表刷新(断线期间
|
|
181
|
+
列表可能已经落后),daemon 全程没起、流从未连上过的情形同样补——否则活动条绿了而
|
|
182
|
+
列表还停在错误态,看着像好了其实没好。
|
|
175
183
|
- **聚合选择(临时多选聚合日志)**:工具条的 `聚合选择` 进入选择态——每张卡片
|
|
176
184
|
左侧出现勾选框,点卡片本体变成**勾选 / 取消**(不再进详情;动作条暂时收起,
|
|
177
185
|
免得一边多选一边误点启停删),工具条与列表之间出现操作条:「已选 N 个容器」+
|
|
@@ -347,6 +355,61 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
347
355
|
`permission denied while trying to connect to the Docker daemon socket`
|
|
348
356
|
之类的错误。
|
|
349
357
|
|
|
358
|
+
#### 连接本机(一键)
|
|
359
|
+
|
|
360
|
+
不想手填目标时,UI 里有两个入口,都**不做常驻**:
|
|
361
|
+
|
|
362
|
+
| 何时 | 入口在哪 |
|
|
363
|
+
| --- | --- |
|
|
364
|
+
| 一个目标都没有 | 面板正文的空态里一个 **连接本机** 主按钮(那时整块是空的,一键就是最短路径) |
|
|
365
|
+
| 已经有别的目标 | **目标下拉的末一项** `+ 连接本机`——就在你找机器的地方,点开才出现 |
|
|
366
|
+
|
|
367
|
+
两处互斥(不会同一个入口出现两次):没有目标时走空态那个大按钮;有目标时下拉里才有那一项。
|
|
368
|
+
**已经有本机目标时两处都不出现**(选择器里已经有那台本机了,再列一项等于重复)。
|
|
369
|
+
|
|
370
|
+
agent 侧同一件事是 `docker_connect_local` 工具,HTTP 侧是 `POST /connect-local`——
|
|
371
|
+
所有入口共用宿主里同一份实现。
|
|
372
|
+
|
|
373
|
+
为什么不做成常驻的工具条按钮:常驻一个「连接本机」既占掉目标选择器右边的位置,又要靠
|
|
374
|
+
强调色招人点,而**已经有可用目标**时它是纯噪音(第一版就是常驻的,用户当场指出多余)。
|
|
375
|
+
但只留空态那一个入口又会漏掉「手上有几台 SSH、想再加一台本机」这种常见情况——所以
|
|
376
|
+
第二个入口放进下拉,只有在真的去找机器时才出现。
|
|
377
|
+
|
|
378
|
+
| 情况 | 行为 |
|
|
379
|
+
| --- | --- |
|
|
380
|
+
| 已有一条 `kind=local` 目标(无论叫什么名字) | **复用它**(`created:false`),只切过去;不新建、不重写、不改名 |
|
|
381
|
+
| 没有本机目标 | 用稳定名 `local` 追加到列表末尾;`local` 已被别的目标占用时依次试 `local-2`、`local-3`…(确定性、不冲突、不覆盖) |
|
|
382
|
+
| 已有 SSH / 自定义目标 | **原样保留**(含顺序与凭据);只在末尾追加本机那条 |
|
|
383
|
+
| 重复点 / 并发点 | 幂等:最终只有一条本机目标 |
|
|
384
|
+
| 设置存储不可用 / 保存失败 | 明确报错(503 / 500 + 原因),**不改内存配置**——不会出现「界面上加上了、刷新就没了」 |
|
|
385
|
+
|
|
386
|
+
写盘成功之后**顺带做一次只读探测**(`docker version`),结果放进响应的 `message`:
|
|
387
|
+
daemon 可达时给出服务端版本;不可达时把成因说清楚——**docker CLI 缺失**(去装 Docker,
|
|
388
|
+
或把「docker CLI」改成正确路径)/ **探测超时**(daemon 卡死,或 `docker context` 指向了
|
|
389
|
+
连不上的远端;用 `docker context ls` 看当前指向)/ **daemon 未启动**(Linux
|
|
390
|
+
`systemctl start docker`,macOS / Windows 启动 Docker Desktop)/ **socket 无权访问**
|
|
391
|
+
(账号加入 `docker` 组),而不是只回一句「没有容器」。探测失败**不算这次操作失败**
|
|
392
|
+
(目标确实加上了,`reachable:false` 单独表示),下次 daemon 起来就能直接用。
|
|
393
|
+
|
|
394
|
+
探测用的二进制**就是设置卡片里的「docker CLI」**(`dockerBin`,默认 `docker`)。
|
|
395
|
+
CLI 缺失时会**只读地**在 `PATH` 里找一下别的容器 CLI(`docker` → `podman` → `nerdctl`),
|
|
396
|
+
找到就在那句话里点名(「这台机器上装了 podman:把『docker CLI』填成 podman 就能用」)——
|
|
397
|
+
**只提示,绝不替你改配置**:静默改写 `dockerBin` 会违背本插件「不替用户决定」的取向,
|
|
398
|
+
而且 podman 与本插件输出格式的兼容性并未逐项验证(见「已知限制」)。比对候选时按
|
|
399
|
+
**工具名**(basename)而不是配置里的原字符串,所以 `dockerBin` 写成
|
|
400
|
+
`/usr/local/bin/docker` 时不会出现「把 docker 改成 docker」这种废话。
|
|
401
|
+
|
|
402
|
+
超时是**独立一档**:`probe()` 不只报退出码(`code` 为 `null` 时那句「退出码 null」对用户
|
|
403
|
+
零信息量),而是回 `timedOut:true` + 一句可执行的话。超时还会**真的按时返回**——
|
|
404
|
+
SIGKILL 只作用于直接子进程,`dockerBin` 指向包装脚本时孙进程仍握着管道,会把手上的
|
|
405
|
+
15 秒上限拖成分钟级(真机实测 3 秒上限拖到 60 秒),所以到点先杀、再给 500ms 收敛期,
|
|
406
|
+
仍不 `close` 就自行放掉管道收尾。
|
|
407
|
+
|
|
408
|
+
刻意**不做**的事(保守边界):不自动安装 Docker、不启动 daemon、不切换 docker
|
|
409
|
+
context、不改任何环境变量;空 `targets`(= 用户没配)的语义不变,不产生隐式默认目标;
|
|
410
|
+
`clearTargets` 的防误清空逻辑一字未动。这条路径也**与能力开关无关**——它只写插件
|
|
411
|
+
自己的配置,不涉及任何需要授权的目标侧写操作。
|
|
412
|
+
|
|
350
413
|
### 聚合日志(多选 / Compose 项目)
|
|
351
414
|
|
|
352
415
|
多选容器或打开一个 Compose 项目,都能把多个容器的日志聚合成一条流(每个容器一条
|
|
@@ -436,7 +499,7 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
436
499
|
| --- | --- | --- |
|
|
437
500
|
| `enabled` | true | 关闭整个插件(**保存即热生效**:工具立即注销、公告撤下、除 `/config` 外的数据路由 403;`/config` 始终可读写——设置卡片就是重新启用的入口。与 tty 不同:tty 是重启生效) |
|
|
438
501
|
| `announceToAgent` | true | 是否向 agent 注入能力公告(systemPrompt section `plugin:dsh-docker`) |
|
|
439
|
-
| `dockerBin` | `docker` | docker CLI 可执行名或路径(podman 可填 `podman`);只允许字母、数字与 `_ . / \ : -` 及内部空格,且不能以 `-` 开头(**Windows 盘符与 `\`
|
|
502
|
+
| `dockerBin` | `docker` | docker CLI 可执行名或路径(podman 可填 `podman`);只允许字母、数字与 `_ . / \ : -` 及内部空格,且不能以 `-` 开头(**Windows 盘符与 `\` 必须放行**,否则任何绝对路径都填不进来)。**「连接本机」的探测用的就是它**;填错(CLI 不存在)时那句话会点名 `PATH` 里找到的别的容器 CLI,但**不会**替你改这个值 |
|
|
440
503
|
| `allowMutations` | false | 允许**变更操作**:容器 start / stop / restart / remove、镜像删除 / dangling 清理 / 拉取(面板按钮与 `docker_action`、`docker_image_remove`、`docker_image_prune`、`docker_image_pull` 工具;关闭时 `/action`、`/images/remove`、`/images/prune`、`/images/pull/stream` 返回 403,对应工具不注册) |
|
|
441
504
|
| —(能力授权) | 未授权 | `allowMutations` / `allowExec` 有**两条提权通道**:① **启动环境变量**(`DSH_DOCKER_ALLOW_MUTATIONS` / `DSH_DOCKER_ALLOW_EXEC`,值为 `1` / `true` / `yes` / `on`)——判定源是宿主的**启动环境快照**,只认继承来的 `process` 层:写项目 `.env` 或 `~/.dsh/env.yml` **不算**授权;② **就地提权**(免重启):在设置卡片点开关 → 面板给出一条「在宿主终端执行」的命令 → 执行后十秒内生效。HTTP 侧永远可以**关掉**它们(紧急刹车不能依赖重启),但给 `true` 而无授权会被 400 拒绝并说清两条路。配置里的 `true` **不算授权**(它与 HTTP 写进去的值存在同一个存储里,分不出来源)。**升级影响**:升级前靠界面打开的开关会变成关——设环境变量重启,或在卡片里就地确认。**为什么**:回环围栏与同源证明都拦不住跨站页面与页内脚本(它们能自己填 `Sec-Fetch-Site: same-origin`),而 docker socket 等价目标主机 root;细节(含拦不住谁)见 [architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么) |
|
|
442
505
|
| `allowExec` | false | 允许一次性 `docker exec`(面板 exec 输入与 `docker_exec` 工具;关闭时 `/exec` 返回 403) |
|
|
@@ -495,6 +558,7 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
495
558
|
| 工具 | 注册条件 | 参数 | 作用 / 典型用法 |
|
|
496
559
|
| --- | --- | --- | --- |
|
|
497
560
|
| `docker_targets` | 恒注册 | `probe?: boolean` | 列出目标(name / kind / label);`probe:true` 逐个探测 docker 版本与 daemon 可达性(SSH 目标会建连接,较慢)。其他工具的 `target` 取自这里 |
|
|
561
|
+
| `docker_connect_local` | 恒注册 | —(无参数) | **连接本机**:确保存在一条 `kind=local` 目标并选中它(已存在则复用,不重复创建;SSH / 自定义目标原样保留),随后只读探测 daemon:返回 `{name, kind, created, saved, reachable, serverVersion?}`。没有任何目标时 `docker_ps` 等会报「尚未配置任何 Docker 目标」,用它一步补上。**不需要任何能力授权**(只写插件自己的配置 + 只读探测);不会安装 Docker / 启动 daemon / 切 context / 改环境变量。daemon 不可达时在结果文本里说清成因(CLI 缺失 / 探测超时 / daemon 未启动 / socket 无权;CLI 缺失时还会点名 PATH 里找到的别的容器 CLI,但不会替你改 dockerBin),不是只报空列表 |
|
|
498
562
|
| `docker_ps` | 恒注册 | `target?`(**传 `*` = 全部目标**)、`all?: boolean` | 列容器(名称 / 状态 / 健康 / 镜像 / 端口 / compose 项目与服务 / 短 ID);默认只列运行中,`all:true` 含已停止。`target:'*'` 时按目标分组返回,**单个目标不可达不影响其他目标**(该组带 `error`)。端口是 IPv4/IPv6 双栈归并后的映射(同一次 `-p` 不再出现两遍,D130);`ports` 为空**不等于**没暴露端口——host 网络容器的端口即宿主机端口,这种情况会给 `net` 字段(D131)。排障第一步 |
|
|
499
563
|
| `docker_attention` | 恒注册 | `target?`(支持 `*`)、`limit?: number` | **需关注汇总**:不健康 / 反复重启 / 被 OOM 杀 / 非零退出 / 僵死;每条带 `reasons`、`exitCode`、`oomKilled`、`restartCount`。OOM 与真实退出码来自一次 `docker inspect`(ps 摘要里 137 无法区分手动 kill)。排障入口:不确定从哪台/哪个容器看起时先调它 |
|
|
500
564
|
| `docker_inspect` | 恒注册 | `target?`、`id`(必填) | `docker inspect` 的权威详情:状态 / 健康检查 / 退出码 / 重启次数 / 端口 / 挂载 / 网络 / 启动命令 |
|
|
@@ -529,8 +593,8 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
529
593
|
`referrer-policy: no-referrer`。
|
|
530
594
|
|
|
531
595
|
**除 loopback 之外还有一道「同源证明」**:四条 SSE(`/logs/stream`、`/stats/stream`、`/events/stream`、
|
|
532
|
-
`/images/pull/stream
|
|
533
|
-
`/volumes/remove|prune`、`/exec`)要求请求带 `Origin: <同源>` 或 `Sec-Fetch-Site: same-origin`,
|
|
596
|
+
`/images/pull/stream`)与九条写子路由(`/action`、`/images/remove|prune`、`/networks/remove|prune`、
|
|
597
|
+
`/volumes/remove|prune`、`/exec`、`/connect-local`)要求请求带 `Origin: <同源>` 或 `Sec-Fetch-Site: same-origin`,
|
|
534
598
|
否则 403 `缺少同源证明`。这是为了挡住「恶意页面用 `<img src=.../images/pull/stream>` 触发一次真实拉取」
|
|
535
599
|
这类跨站副作用;curl / 老 Safari / 部分 WebView 不带这两个头时会撞上它(浏览器正常使用不受影响)。
|
|
536
600
|
只读路由不要求同源证明。
|
|
@@ -550,6 +614,7 @@ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 →
|
|
|
550
614
|
| `/config` | GET | — | `{ok:true, config}`:配置快照(targets 只给 `passwordSet` / `passphraseSet`,另附只读的 `ttyBooks` / `ttyAvailable` / `toolsRegistered`) |
|
|
551
615
|
| `/config` | POST | 上表配置键的任意子集 | `{ok:true, config}`;未知键 400,非法 JSON 400 |
|
|
552
616
|
| `/targets` | GET / POST | — | `{ok:true, targets:[{name, kind, label?\|error?}]}` |
|
|
617
|
+
| `/connect-local` | POST | `{}`(请求体被忽略) | **一键连接本机**:复用或新建 `kind=local` 目标并返回 `{ok:true, result:{name, kind, created, saved, reachable, serverVersion?, message}, config}`;宿主没有 settings 服务时 503,保存失败时 500(都带原因且不改内存配置)。要求同源证明(它是写路径),但**不需要** `allowMutations` / `allowExec` |
|
|
553
618
|
| `/probe` | POST | `{target?}` | `{ok:true, probe:{ok, bin, serverVersion, error, target}}` |
|
|
554
619
|
| `/containers` | POST | `{target?, all?}` | `{ok:true, containers: ContainerSummary[]}`;`target:'*'` 时返回 `{ok:true, groups:[{target,label,ok,error?,data?}]}`(跨目标并发聚合) |
|
|
555
620
|
| `/attention` | POST | `{target?, limit?}` | 单目标 `{ok:true, items: AttentionItem[], total, truncated, degraded}`(`limit` 在过滤 + 严重度排序**之后**生效,默认 100、上限 500);`target:'*'` 时 `{ok:true, groups:[{target,label,ok,error?,data:{items,total,truncated,degraded}}]}` |
|
|
@@ -760,9 +825,11 @@ abort)、客户端断开静默中止。各自只差执行器与结束原因:
|
|
|
760
825
|
- **变更操作无独立审计日志**:只有 docker 自身的记录与宿主 `ctx.logger` 的
|
|
761
826
|
常规输出。
|
|
762
827
|
|
|
763
|
-
- **跨目标聚合的边界**:并发上限 4
|
|
764
|
-
|
|
765
|
-
|
|
828
|
+
- **跨目标聚合的边界**:并发上限 4;**单目标预算 90s(兜底)**,而每条命令自己的超时
|
|
829
|
+
(30s)才是被遵守的那个——到点时**底层的子进程 / SSH 通道会被真的取消**(不是只让这一格
|
|
830
|
+
返回失败),否则超时那条命令会继续占着远端会话槽(D161)。单个目标失败/超时只影响它自己
|
|
831
|
+
那一格(组里带 `error`)。目标很多时总览的请求量随目标数线性增长(每目标 2 个请求),
|
|
832
|
+
自动刷新会放大这个量——目标多时建议关掉自动刷新。
|
|
766
833
|
|
|
767
834
|
## 工作原理
|
|
768
835
|
|
|
@@ -864,7 +931,7 @@ inspect 解析(状态 / 健康 / 退出码 / 挂载 / 网络 / 端口 / 缺字
|
|
|
864
931
|
`resolveTarget` 的四种路径、`mergeTargetSecrets` 的凭证保留语义。
|
|
865
932
|
|
|
866
933
|
`scripts/route-smoke.mjs` 用**假 cordis ctx + 假 docker CLI 脚本**跑端到端:
|
|
867
|
-
插件挂载(settings / 工具 / 路由 / 能力公告注册)、**
|
|
934
|
+
插件挂载(settings / 工具 / 路由 / 能力公告注册)、**27 条路由**的实际调用与返回
|
|
868
935
|
(含 `/logs/stream`、`/stats/stream`、`/events/stream`、`/images/pull/stream` 四条 SSE
|
|
869
936
|
的事件序列与参数校验,事件流另断噪音被白名单丢掉)、`docker_events` 工具的快照
|
|
870
937
|
输出与 `since` 字符集校验、`/images/inspect` 的详情 + 构建历史、`/config` 凭证脱敏、未知配置键
|
|
@@ -942,15 +1009,28 @@ network / volume 的 ls·inspect 解析容错(字符串布尔、缺 `Mountpoin
|
|
|
942
1009
|
2. **SSH 目标**:tty 连接簿里已有条目时,用 `book` 引用它 → 容器列表 / 详情 /
|
|
943
1010
|
日志正常;首次连接日志里出现「已记录 host key 指纹(TOFU)」,第二次不再
|
|
944
1011
|
提示;手动改掉 `hostKeys` 里的指纹后重连,应**被拒绝**并给出重置指引。
|
|
945
|
-
3.
|
|
946
|
-
|
|
947
|
-
|
|
1012
|
+
3. **一键连接本机**(两处入口都要试):
|
|
1013
|
+
- **没有目标时**:把目标全部删空保存(或用一个全新 profile),打开面板 → 正文空态里
|
|
1014
|
+
有一个「连接本机」主按钮,**下拉里不该重复列**这一项 → 点一下应当出现一条
|
|
1015
|
+
`kind=local` 的 `local` 并立刻选中、容器列表出来;
|
|
1016
|
+
- **已有别的目标时**:留着几台 SSH,打开目标下拉 → 末一项是 `+ 连接本机`,
|
|
1017
|
+
**工具条上不该有**任何常驻入口 → 选中该项,应当同样加上 `local` 并切过去,
|
|
1018
|
+
且该项随即从下拉里消失(已经有本机目标了)。
|
|
1019
|
+
两处都**不该**出现「同一个入口列了两次」;全程**不该**有任何 Docker 安装 / daemon 启动 /
|
|
1020
|
+
context 切换动作。宿主上没有 docker 时,应当看到一句点名的原因(CLI 缺失 / 探测超时 /
|
|
1021
|
+
daemon 未启动 / socket 无权访问),而不是一个空列表。
|
|
1022
|
+
- **只有 podman 的机器**(把 PATH 里的 docker 拿掉、只留 podman):那句话应当
|
|
1023
|
+
**点名 podman** 并说明「把『docker CLI』填成 podman 就能用」,同时**不该**替你
|
|
1024
|
+
改 `dockerBin`(回头去设置卡片看,值仍是 `docker`)。
|
|
1025
|
+
4. **只读拦截**:两个开关都关时,`/action`、`/exec`、`/images/remove`、
|
|
1026
|
+
`/images/prune`、`/images/pull/stream` 全部 403;agent 侧恒注册 12 个只读工具
|
|
1027
|
+
(`docker_targets` / `connect_local` / `ps` / `attention` / `inspect` / `logs` / `stats` / `events` /
|
|
948
1028
|
`images` / `image_inspect` / `networks` / `volumes`,数量以工具表为准),
|
|
949
1029
|
面板的启停删 / 镜像删除 / 清理 / 拉取按钮置灰。打开「允许变更操作」后这些
|
|
950
1030
|
路由与工具立即出现(无需重启)。
|
|
951
|
-
|
|
1031
|
+
5. **日志 / 统计 / 镜像**:`tail` 与 `timestamps` / `since` 生效;统计显示
|
|
952
1032
|
CPU、内存、网络与块 IO;镜像列表含 dangling 条目标记。
|
|
953
|
-
|
|
1033
|
+
6. **FOLLOW 实时日志流**:日志页打开 `FOLLOW` → 状态行先「正在连接」后
|
|
954
1034
|
「实时跟随中」,`docker logs -f` 的新行即时出现(`docker run --rm alpine sh
|
|
955
1035
|
-c 'i=0; while :; do echo line-$i; i=$((i+1)); sleep 1; done'` 可观察);
|
|
956
1036
|
高吞吐护栏:换成长行快速打印(如 `head -c 2000 /dev/zero | tr "\\0" x; echo` 循环)
|
|
@@ -961,61 +1041,61 @@ network / volume 的 ls·inspect 解析容错(字符串布尔、缺 `Mountpoin
|
|
|
961
1041
|
(或热改插件配置)→ 状态行短暂「正在重连」后自愈,不弹错误横幅。SSH 目标
|
|
962
1042
|
同样跑一遍,确认流跑着时同一主机的 `docker ps` 面板操作不受影响(连接复用),
|
|
963
1043
|
且空闲回收(120s)不会掐断流。
|
|
964
|
-
|
|
1044
|
+
7. **统计实时跟随**:统计页打开 `FOLLOW` →「正在连接统计流…」→「实时跟随中
|
|
965
1045
|
(docker stats)」,CPU / 内存 sparkline 每秒长一点(跑 `docker run --rm
|
|
966
1046
|
alpine sh -c 'while :; do :; done'` 观察 CPU 起来);关 FOLLOW 立即回快照并
|
|
967
1047
|
恢复轮询。停掉被统计的容器 → 流收到 `end`(stats-exit)提示后自动回轮询。
|
|
968
1048
|
注意**这条流不会自然结束**:切页 / 关面板必须断掉 `EventSource`(宿主侧应
|
|
969
1049
|
看到 `docker stats` 被 SIGTERM)。
|
|
970
|
-
|
|
1050
|
+
8. **镜像详情 / 删除 / 清理**:镜像页点某行「详情」→ 概览里的层数与
|
|
971
1051
|
`docker image inspect` 一致、构建历史与 `docker history` 一致(Docker ≥ 26
|
|
972
1052
|
走 `--format`,老版本走纯文本表格兜底);dangling 行用镜像 ID 查。点「删除」→
|
|
973
1053
|
二次确认后执行 `docker image rm`;删除被容器引用的镜像应失败并给出提示。
|
|
974
1054
|
「清理 dangling」只删无标签镜像,输出末尾带 `Total reclaimed space`。
|
|
975
|
-
|
|
1055
|
+
9. **拉取进度流**:镜像页工具条的拉取图标(悬停显示「拉取镜像」)→ 输入本地没有的小镜像(如 `alpine:3.20`)
|
|
976
1056
|
→ 逐层状态行实时出现且按层原地更新;完成后提示「拉取完成」并自动刷新列表。
|
|
977
1057
|
拉取中点「停止」或离开视图 → `docker pull` 被 SIGTERM 结束,不残留进程。
|
|
978
1058
|
未开启「允许变更操作」时该按钮置灰、直接访问 `/images/pull/stream` 返回 403。
|
|
979
|
-
|
|
1059
|
+
10. **Compose 项目视图**:用一份 compose 起两三个服务(`docker compose up -d`)→
|
|
980
1060
|
工具条切到「Compose」→ 服务归到一个项目卡片下,运行数 / 服务数 / 状态正确;
|
|
981
1061
|
点进项目看服务表,切「聚合日志」→ 各服务日志按 `[service]` 前缀混流出现
|
|
982
1062
|
(`docker compose logs -f` 的等价物),过滤框可按服务名与内容过滤;关「自动
|
|
983
1063
|
滚动」后新日志继续进缓冲但视图不跳。无 compose 标签的容器归入
|
|
984
1064
|
「(非 compose 容器)」。
|
|
985
|
-
|
|
1065
|
+
11. **`allowMutations` 打开后**:stop / start / restart 成功;对运行中容器
|
|
986
1066
|
remove 报错并附「先停止再删除」提示,先 stop 再 remove 成功。
|
|
987
|
-
|
|
1067
|
+
12. **`allowExec` 打开后**:`ls -la /app` 之类命令返回 stdout 与退出码;把命令
|
|
988
1068
|
换成 `sleep 60`(`timeoutSec` 调小)应被中断并报超时;`command` 超过 8000
|
|
989
1069
|
字符被拒绝。
|
|
990
|
-
|
|
1070
|
+
13. **exec 抽屉与正文共存**(tty ≥ 0.15):卡片「终端」开抽屉后切到日志 / 统计 /
|
|
991
1071
|
另一台容器的详情,抽屉与终端会话都必须活着;**折叠**(箭头或双击顶边)把抽屉
|
|
992
1072
|
压成一条标题栏、会话继续跑(展开即回原样);**拖拽顶边**能调高度且不超过面板
|
|
993
1073
|
的 75%;此时点 backdrop 空白处或面板 ✕ 应弹「结束容器终端会话」确认,取消后
|
|
994
1074
|
会话仍在,确认才结束(tty 侧收到 kill)。
|
|
995
1075
|
离线回归:`node packages/tty/scripts/preview.mjs docker-exec-logs`
|
|
996
1076
|
(无头 Chrome 跑真实客户端:抽屉 + 日志页 + 折叠展开,断言会话未被结束)。
|
|
997
|
-
|
|
1077
|
+
14. **从连接栏进容器面板(dock 模式,需 tty ≥ 0.16)**:tty 面板里开一个 SSH 标签 →
|
|
998
1078
|
连接栏「容器」→ 容器面板应挂在**终端右侧**(不是全屏弹窗),终端仍可输入;
|
|
999
1079
|
拖左边缘可调宽(上限面板宽 72%)、标题栏箭头折叠成窄条(终端拿回全部宽度、
|
|
1000
1080
|
容器面板不卸载)、✕ 收起面板且 SSH 标签不受影响;此时点卡片「终端」应在同一
|
|
1001
1081
|
终端面板**新开一个 `<容器> · exec` 标签**,而不是再嵌一个终端抽屉。
|
|
1002
1082
|
离线回归:`node packages/tty/scripts/preview.mjs docker-dock`
|
|
1003
1083
|
(断言 docked / 无 backdrop / 折叠后终端变宽 / 容器面板存活)。
|
|
1004
|
-
|
|
1084
|
+
15. **多选聚合日志**:容器列表点 `聚合选择` → 卡片左侧出现勾选框、卡片动作条收起;
|
|
1005
1085
|
勾 2~3 个容器 → 操作条显示「已选 3 个容器」,点 `聚合日志` → 聚合视图标题为
|
|
1006
1086
|
「聚合日志 · 3 个容器」,三条流按 `[service]` / 容器名前缀混流出现;只勾 1 个时
|
|
1007
1087
|
按钮置灰并提示「至少选择 2 个容器」,勾到 9 个时置灰并提示最多 8 个;列表里
|
|
1008
1088
|
停掉其中一个容器 → 该条流走原有 `end` 语义结束,其余流不受影响。
|
|
1009
|
-
|
|
1089
|
+
16. **Esc 退出选择态**:选择态下按 Esc → 回到普通列表、勾选清空、动作条消失;
|
|
1010
1090
|
再点一次 `聚合选择`(此时标签是 `退出选择`)效果相同;切目标 / 切分段 / 关面板
|
|
1011
1091
|
也都会把选择态和勾选一起清掉。
|
|
1012
|
-
|
|
1092
|
+
17. **事件活动条**:容器列表头部出现「活动」条,状态点先黄后绿、文案「实时接收中
|
|
1013
1093
|
(docker events)」;执行 `docker restart <容器>` / `docker stop`+`start` → 1 秒内
|
|
1014
1094
|
列表状态跟着变,活动条出现 `stop` / `start`(异常退出显示 `die(137)` 这种带码
|
|
1015
1095
|
形式);点标题可折叠(事件流不中断,展开后仍是累积的最新 8 条);
|
|
1016
1096
|
`docker exec` 连续几次**不产生任何事件**(`exec_*` 已被服务端白名单丢掉);
|
|
1017
1097
|
断网 / 重启 `dsh web` 后恢复时状态点短暂黄色并自动补一次全量列表刷新。
|
|
1018
|
-
|
|
1098
|
+
18. **网络 / 卷**:工具条分段出现「网络」「卷」。网络列表应含 `bridge` / `host` / `none`
|
|
1019
1099
|
与 compose 创建的项目网络;点某条进详情 → 概览里子网 / 网关与 `docker network
|
|
1020
1100
|
inspect` 一致、`internal` 网络带徽标,「接入的容器」页签列出容器名与 IPv4。
|
|
1021
1101
|
卷列表的挂载点与 `docker volume ls` 一致(超长路径省略、悬停看全量)。
|
|
@@ -1024,7 +1104,7 @@ network / volume 的 ls·inspect 解析容错(字符串布尔、缺 `Mountpoin
|
|
|
1024
1104
|
卷清理会弹带「数据会一起删除」的确认框。**注意**:卷清理在 docker < 23 上会连
|
|
1025
1105
|
命名卷一起删,先在测试目标上确认为妙。
|
|
1026
1106
|
|
|
1027
|
-
|
|
1107
|
+
19. **多目标总览**:配两个以上目标(本机 + 一台 SSH)→ 目标选择器旁出现 `总览` pill,
|
|
1028
1108
|
点进去一屏出现两张计数卡(各带「本机」/「SSH」徽标)与异常区;卡片数字与该目标
|
|
1029
1109
|
容器列表的口径一致。把其中一个目标的 SSH 地址改错(或停掉远端 docker)再刷新 →
|
|
1030
1110
|
只有那张卡描红、顶部出现「1 个目标不可达」,另一张卡照常显示结果,**页面不会**
|
|
@@ -1032,7 +1112,7 @@ network / volume 的 ls·inspect 解析容错(字符串布尔、缺 `Mountpoin
|
|
|
1032
1112
|
**该目标**的容器列表而不是总览;点计数卡 → 该目标的容器列表。全部正常时异常区显示
|
|
1033
1113
|
「一切正常」;还有目标没答完时显示「读取中…」。打开「自动刷新」→ 按
|
|
1034
1114
|
`pollIntervalSec` 轮询全部目标(关掉即停)。
|
|
1035
|
-
|
|
1115
|
+
20. **切目标不串台(列表写入闸)**:让 目标1(不可达的 SSH)先失败出「操作失败」横幅,
|
|
1036
1116
|
随即切到正常的目标2 → 横幅应**随切换立即消失**、列表是 目标2 的容器;再等 20s
|
|
1037
1117
|
让 目标1 的超时响应**在其之后**才回来 → 横幅不得重新出现、列表也不得被换成
|
|
1038
1118
|
目标1 的容器。从总览点 目标1 那张红色计数卡进去,同样不该看到上一个目标的失败。
|