dsh-home-hosted 0.6.3 β†’ 0.7.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.md CHANGED
@@ -2,18 +2,21 @@
2
2
 
3
3
  # πŸ“Œ dsh-home-hosted
4
4
 
5
- **Your whole home stack, up after every reboot β€” your `dsh` web server included.**
5
+ **Prompt your servers into existence β€” keep them running and manage them via dsh / `home-hosted` panel UI.**
6
+
7
+ <sub>Your whole home stack, up after every reboot β€” your `dsh` web included.</sub>
6
8
 
7
9
  A [DeepSeek Harness](https://github.com/deepseek-ai/dsh) plugin for
8
10
  [home-hosted](https://github.com/NamesMT/home-hosted), the panel that supervises your services.
9
- Declare the servers once; this page manages the panel, the boot entry and the entries.
11
+ Ask for a Gitea, a Jellyfin, an FTP server; `dsh-home-hosted` sets up the entries and manages them
12
+ β€” panel, ports, restarts and boot autostart included.
10
13
  <sub>Nothing is installed or started until you say so.</sub>
11
14
 
12
15
  [![npm](https://img.shields.io/npm/v/dsh-home-hosted?label=npm&color=blue)](https://www.npmjs.com/package/dsh-home-hosted)
13
16
  [![CI](https://github.com/NamesMT/dsh-home-hosted/actions/workflows/ci.yml/badge.svg)](https://github.com/NamesMT/dsh-home-hosted/actions/workflows/ci.yml)
14
17
  [![license](https://img.shields.io/npm/l/dsh-home-hosted?color=green)](./LICENSE)
15
18
  [![node](https://img.shields.io/badge/node-%3E%3D24-339933?logo=node.js&logoColor=white)](https://nodejs.org)
16
- [![dsh](https://img.shields.io/badge/dsh-%3E%3D0.1.7--rc.2-5b21b6)](https://github.com/deepseek-ai/dsh)
19
+ [![dsh](https://img.shields.io/badge/dsh-%3E%3D0.2.0--rc.1-5b21b6)](https://github.com/deepseek-ai/dsh)
17
20
 
18
21
  [πŸš€ Quick start](#-quick-start) Β· [πŸŽ›οΈ What the plugin does](#-what-the-plugin-does) Β· [πŸ€– Agent tools](#-agent-tools) Β· [🧩 Depth](#-depth)
19
22
 
@@ -35,52 +38,59 @@ Declare the servers once; this page manages the panel, the boot entry and the en
35
38
  dsh plugin --profile web add dsh-home-hosted
36
39
  ```
37
40
 
38
- Open **Settings β†’ Home Hosted**, turn on **Manage dsh**, and enable autostart.
39
- <sub>Reboot once; `dsh` comes back without you touching anything.</sub>
41
+ Open **Settings β†’ Home Hosted** and add your servers β€” or just ask the agent:
42
+
43
+ > add a Gitea on 3000, and a Jellyfin on 8096
44
+
45
+ Then, optionally, turn on **Manage dsh** and **Autostart**, so the panel β€” and everything in it β€”
46
+ comes back after a reboot.
40
47
 
41
48
  ## πŸ€” Why
42
49
 
43
- A panel restart shouldn't kill a prompt mid-flight, and a reboot shouldn't cost you an SSH session.
50
+ Ever tried setting up a media server, a Gitea and a few more yourself β€” then keeping them all
51
+ managed?
52
+
53
+ `dsh-home-hosted` uses [home-hosted](https://github.com/NamesMT/home-hosted) to do it: prompt for
54
+ the servers you want, let it write and configure the entries, and manage them all from dsh or the
55
+ panel β€” ports, health, restarts and autostart included.
44
56
 
45
57
  ```text
46
- reboot
47
- β”‚
48
- β–Ό
49
- OS boot entry ──▢ home-hosted panel ──▢ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
50
- systemd Β· β”‚ β”‚ dsh web βœ“ β”‚
51
- launchd Β· β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
52
- XDG Β· Run key β”‚
53
- β”œβ”€β”€β”€β”€β”€β”€β”€β”€β–Ά β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
54
- β”‚ β”‚ docker β”‚
55
- β”‚ β”‚ compose β”‚
56
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
57
- β”‚
58
- └────────▢ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
59
- β”‚ postgres β”‚
60
- β”‚ :5432 β”‚
61
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
58
+ you ──▢ "add a Gitea, a Jellyfin, a cache…"
59
+ β”‚
60
+ β–Ό
61
+ dsh-home-hosted ──▢ home-hosted panel ──▢ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
62
+ page Β· agent β”‚ β”‚ gitea :3000 β”‚
63
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
64
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ά β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
65
+ β”‚ β”‚ jellyfin β”‚
66
+ β”‚ β”‚ :8096 β”‚
67
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
68
+ └──────────▢ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
69
+ β”‚ postgres β”‚
70
+ β”‚ :5432 β”‚
71
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
72
+
73
+ β˜… OS boot entry ──▢ the panel comes back, and so does everything in it
74
+ systemd Β· launchd Β· XDG Β· Run key
62
75
  ```
63
76
 
64
77
  **`dsh` is one entry in the panel, not the whole product.** The panel is the supervisor: it starts
65
- each entry, watches it, restarts what dies and reclaims its port. Add your compose stack, your
66
- database, your bot β€” manage them from the page, or hand the entry ids to an agent.
78
+ each entry, watches it, restarts what dies and reclaims its port. Add your Gitea, your Jellyfin,
79
+ your compose stack, your database β€” manage them from the page, or hand the entry ids to an agent.
67
80
 
68
- A plugin cannot run at boot, so this one writes the OS entry while dsh runs and the OS takes over.
69
- One managed entry (`dsh`) is kept alive under home-hosted's nanny, so a panel restart leaves it
70
- running.
81
+ Managing `dsh` itself is the bonus: the entry that keeps your harness up is a highlight on top of
82
+ the server management, not the thing this plugin is.
71
83
 
72
84
  ## πŸŽ›οΈ What the plugin does
73
85
 
74
86
  | | |
75
87
  |---|---|
76
- | πŸš€ **Boot autostart** | Installs, verifies and removes the OS entry. Opt in per machine. |
77
- | πŸ›‘οΈ **Survives a panel restart** | The managed entry runs under home-hosted's nanny, so restarting the panel leaves it running. |
78
- | πŸ“¦ **Ships its own panel** | The pinned `home-hosted` is a dependency, so boot runs the version this plugin is tested against. |
79
- | πŸ–₯️ **Server control** | Add, edit, start, stop and restart entries β€” written through the panel's API, so nothing restarts behind your back. |
80
- | πŸ—‚ **Workspaces** | The panel manages one workspace for the `dsh` entry; the page and the agent tools reach **every** workspace it serves, counts included. |
88
+ | πŸ–₯️ **Servers, set up by prompt** | Add, edit, start, stop and restart entries β€” Gitea, Jellyfin, FTP, a compose stack. Written through the panel's API, so nothing restarts behind your back. |
89
+ | 🏠 **The whole home-hosted panel** | Not just servers: [BYOU](https://github.com/NamesMT/home-hosted#-bring-your-own-ui-byou) custom UIs, [workspaces](https://github.com/NamesMT/home-hosted#-workspaces), a [reverse proxy](https://github.com/NamesMT/home-hosted/blob/main/docs/REVERSE_PROXY.md) with automatic HTTPS, [backups](https://github.com/NamesMT/home-hosted#-backups) (zip or AES-256), [notifications](https://github.com/NamesMT/home-hosted/blob/main/docs/NOTIFICATIONS.md), [dynamic DNS](https://github.com/NamesMT/home-hosted/blob/main/docs/DDNS.md), health probes, live logs and host vitals. |
90
+ | πŸ€– **Agent tools** | On by default, session permissions still gate every write. |
91
+ | πŸš€ **Boot autostart** | *Highlight:* installs, verifies and removes the OS entry, so the panel starts at boot. Opt in per machine. |
81
92
  | 🧭 **Token warnings that mean something** | A missing or refused API token is called out on the page, with one click to mint a working one. |
82
93
  | πŸͺŸ **Knows the other panels** | Several panels on one machine? The agent is told which one this plugin manages, and asks before touching another. |
83
- | πŸ€– **Agent tools** | On by default, session permissions still gate every write. |
84
94
 
85
95
  ## πŸ€– Agent tools
86
96
 
@@ -141,6 +151,18 @@ re-finds your build at boot, so a rebuild, a moved checkout or a fresh profile d
141
151
 
142
152
  </details>
143
153
 
154
+ <details>
155
+ <summary><b>dsh Desktop</b></summary>
156
+
157
+ The Desktop client is supported, with one exception. Everything server-side works as it does in the
158
+ browser: the page, the agent tools, workspaces, the panel, and boot autostart.
159
+
160
+ **Manage dsh** is web-only and shown disabled there, because Desktop starts its own reserved profile
161
+ in Electron β€” it is never one of the panel's server entries. (`dsh --profile desktop` is refused by
162
+ the CLI, so a row for it could only crash-loop.) Server entries of every other kind are unaffected.
163
+
164
+ </details>
165
+
144
166
  <details>
145
167
  <summary><b>Managed entry and port policy</b></summary>
146
168
 
@@ -148,6 +170,7 @@ One entry (`dsh`), one toggle: the panel keeps it alive, restarts it and reclaim
148
170
  detached restart is recognised on macOS and Linux (`follow`); Windows cannot prove identity through
149
171
  a `.cmd` shim, so it uses `onPortConflict: kill`. The plugin pins home-hosted 0.7+, so every key it
150
172
  writes is one that panel parses.
173
+ <sub>Web only β€” see **dsh Desktop** above.</sub>
151
174
 
152
175
  </details>
153
176
 
@@ -0,0 +1,30 @@
1
+ # Agent tools
2
+
3
+ What the model is given, and what gates it.
4
+
5
+ Related: [server entries](ENTRIES.md)
6
+
7
+ ## Agent tools are merged and on by default
8
+
9
+ Seven tools, not ten: `servers_lifecycle` carries start/stop/restart, `servers_edit` carries
10
+ create/update/delete, `autostart_manage` carries install/uninstall, `ui_manage` drives the panel's own
11
+ UI, and `workspaces_list` is the read-only way to see every workspace. They register on by default;
12
+ what gates a mutating call is the session's own permission mode, not a plugin-level default.
13
+
14
+ A description is read at the approval prompt, so each is one line a person can glance at: what the tool
15
+ does, and only the consequence they could not predict (restarting this session's own entry ends it;
16
+ installing an entry stops the panel). Mechanics belong in the argument schemas and the docs.
17
+
18
+ Structured parameters are declared as objects, never as `type: 'json'`: an author-only `json` node
19
+ projects to a schema with no `type` at all, and a real session then delivers something the handler
20
+ cannot read β€” `create`/`update` were unusable because of it while every test passed, since tests call
21
+ the handler directly. A declared object is validated by the runtime before the handler, so a wrong
22
+ shape is rejected with the parameter named.
23
+
24
+ ## Approvals follow the session's sandbox
25
+
26
+ A mutating agent tool asks the approval service only when the calling session is *not* already
27
+ `danger-full-access` (`ctx.sandboxPolicy.resolve({ session })`). Asking anyway made a Full-access run
28
+ fail whenever the deployment's approvals auto-reject β€” the session had already granted exactly what the
29
+ tool was asking about. Below full access the tool still asks and still fails closed, and the refusal
30
+ names the mode and the remedy.
package/docs/BOOT.md ADDED
@@ -0,0 +1,154 @@
1
+ # Boot autostart
2
+
3
+ How the panel comes back after a reboot, and the account it comes back as.
4
+
5
+ Related: [the panel](PANEL.md) Β· [server entries](ENTRIES.md)
6
+
7
+ ## A plugin cannot act at boot
8
+
9
+ The plugin runs inside dsh. At boot nothing runs yet, so autostart cannot be a runtime behaviour: the
10
+ plugin installs an OS entry while dsh runs, and the OS starts home-hosted from then on. Every plugin
11
+ start re-asserts that entry (rewrite-if-changed), which is also what repairs a node path that moved
12
+ after an upgrade.
13
+
14
+ ## One boot artifact per state root
15
+
16
+ The boot artifact name follows the state root too, keyed on the *machine* default: `home-hosted` only
17
+ for `~/.dsh/dsh-home-hosted`, `home-hosted-<hash>` otherwise. Keying it on the running process's
18
+ `$DSH_HOME` would name every install's default state dir `home-hosted` and reintroduce the collision.
19
+ An install that used to write the old name retires it once its own artifact is in place, and only an
20
+ artifact carrying this plugin's marker is ever removed.
21
+
22
+ ## Boot entries run the panel as a person
23
+
24
+ Two mechanisms drop privilege by name β€” a systemd `User=` and a launchd `UserName` β€” and both used to
25
+ take that name from `$USER`. That is right in the common case and wrong in the one case it exists for:
26
+ `sudo` and `pkexec` rewrite `USER`, `LOGNAME` and `HOME` to the *target* account, so a panel started
27
+ from a root shell read `USER=root`, and a mechanism that already runs as root got an entry that either
28
+ repeated root or, with the name dropped, silently kept it. `os.userInfo()` is no help: it reports the
29
+ euid.
30
+
31
+ So the account comes from whoever this process is. An elevating tool's `SUDO_UID`/`PKEXEC_UID` is an
32
+ instruction and is used while it resolves, so a stale value inherited from an ancestor cannot collapse
33
+ the entry to root; otherwise this session's own login β€” its database row, else the row for this uid;
34
+ otherwise the owner of the state root or the panel root, which names the person a root-launched panel
35
+ belongs to; and only then, honestly, root. The uid is the fact and `$UID` is a claim: a shell variable
36
+ rather than an exported one, so when it is present at all it was inherited. It is read only when the
37
+ platform gives no uid, and it also feeds the launchd domain (`gui/$UID`).
38
+
39
+ Two answers are refused rather than written, and both fail loudly:
40
+
41
+ - **`User=root`**, because an entry with no `User=` already runs as root β€” naming it only dresses up the
42
+ silent default this change exists to surface;
43
+ - **a name the user database cannot place.** systemd matches names case-sensitively and cannot resolve
44
+ what it does not know, so `User=<such a name>` makes the unit refuse to start (`status=217/USER`) and
45
+ `Restart=always` turns that into a crash-loop that never mentions the account. An LDAP or NIS login
46
+ has no `/etc/passwd` row, so those operators set `User=` themselves; the warning says so.
47
+
48
+ `HOME` is written into every spec, because `Environment=HOME=` **overrides `User=`** β€” verified on real
49
+ systemd. So it has to be the account's home: left at the installer's it is root's under `sudo`, and the
50
+ panel would run as the person with `~` pointing at `/root`, dying with a permission error that never
51
+ mentions its own user. It is written only when it differs, so an unchanged install stays byte-identical
52
+ and startup `reconcile()` does not rewrite the entry. When an account resolves but its home is not
53
+ knowable, the line is **omitted** instead: systemd then supplies the right one from NSS.
54
+
55
+ An installed entry that already runs as the wrong account is reported by `status()`, and the
56
+ `enabled-failing` repair heals it by installing again. A *working* entry is deliberately left alone:
57
+ re-installing stops the panel and hands it to the entry, which is a person's decision.
58
+
59
+ ## KillMode=process: a restart must not take the entries with it
60
+
61
+ `persistent: true` puts an entry under a nanny the panel spawns, and the nanny is what makes it outlive
62
+ the panel: it owns the child's pipes, so a panel stop, restart or kill cannot break them. The nanny is
63
+ spawned `detached`, which gives it its own session β€” but *not* its own cgroup, and systemd's default
64
+ `KillMode=control-group` signals every process in the unit's cgroup. So a `systemctl stop`/`restart` β€”
65
+ which is what this plugin's own activation, and any `Restart=always`, runs β€” takes the nanny down with
66
+ the panel, quietly falsifying the guarantee the design depends on. `KillMode=process` signals the unit's
67
+ main process alone.
68
+
69
+ That main process is the generated launcher, not the panel, so the launcher has to hand a signal on: it
70
+ `spawn`s the CLI and forwards `SIGTERM`/`SIGINT`/`SIGQUIT`/`SIGHUP` to it, then exits with the child's
71
+ outcome. A `spawnSync` launcher cannot do this β€” it is blocked in the syscall when the signal lands, so
72
+ the signal kills the launcher and orphans the panel while the unit exits and `Restart=` loops on
73
+ "already running". The generated launchers are covered by an execution test that signals the real
74
+ artifact and asserts the panel received the stop.
75
+
76
+ An ordinary entry is spawned `detached` too, so the same applies to every supervised server and not only
77
+ the nannies: the panel re-adopts a survivor by its port, which is what makes keeping them the right
78
+ answer rather than a leak. It belongs on both units.
79
+
80
+ ## macOS: agent or daemon
81
+
82
+ A `LaunchAgent` starts at login; starting before login means a `LaunchDaemon` in
83
+ `/Library/LaunchDaemons`, which is root-owned and needs a one-time `sudo`. The daemon is therefore
84
+ offered even when this process cannot elevate: install stages the plist and returns the three commands
85
+ to run. The daemon plist names the invoking user in `UserName`, Apple's sanctioned way to avoid running
86
+ the panel as root β€” resolved by the rule above, never from `$USER`. The recommendation still prefers a
87
+ mechanism this process can install on its own so "Automatic" stays a single click.
88
+
89
+ Reachable launchd is not a requirement: a plist in `~/Library/LaunchAgents` is loaded at the next login
90
+ whether or not the process that wrote it can reach `gui/$UID` (SSH sessions often cannot). So the agent
91
+ stays `available` and its install reports "loads at the next login" instead of refusing; only the
92
+ `bootstrap`/`enable`/`print` steps need the domain, and they are skipped when it is unreachable. The
93
+ daemon scope still needs root for `/Library/LaunchDaemons`.
94
+
95
+ ## Enabling autostart hands the panel over
96
+
97
+ An install alone proves nothing. The panel the plugin started keeps running, so nothing shows whether
98
+ the entry would ever start one β€” and the CLI refuses to start a second panel while `run.json` names a
99
+ live pid, so `enable --now` on a freshly written unit leaves it *failed* while an orphan panel holds the
100
+ port. So a successful install hands the running panel to the entry: the panel stops, and the entry
101
+ starts it. That is the only way to know autostart works rather than merely that a file was written.
102
+
103
+ The stop takes the panel's servers with it, and this plugin is usually one of them, so the work goes to
104
+ a generated detached helper (`bin/panel-activate.mjs`) that outlives the process. Its order is the
105
+ whole design:
106
+
107
+ 1. prove the start can work β€” the unit file the plugin wrote exists and `systemctl is-enabled` agrees β€”
108
+ while the panel is still up, because a start that could never work must not be attempted once the
109
+ old panel is gone. It proves *installed and enabled*, never *already active*: `enable --now` has
110
+ just started the unit while the panel still holds `run.json`, and home-hosted refuses a second
111
+ panel, so the unit is `activating` or `failed` at exactly the moment this runs;
112
+ 2. on a switch, stop the panel through the *previous* mechanism (`systemctl stop`, `launchctl kill`):
113
+ its own restart policy (`Restart=always`, `KeepAlive`) would bring the panel back the moment `down`
114
+ returned, racing the new entry;
115
+ 3. stop it with the CLI's `down`, then wait for the pid, escalating to SIGTERM and SIGKILL;
116
+ 4. start the entry;
117
+ 5. only then remove the other mechanisms' entries.
118
+
119
+ The stop is always the CLI's `down`, never the new mechanism's own `restart`: the entry would then be
120
+ starting a *second* panel, which home-hosted refuses while `run.json` names a live pid, so a
121
+ restart-in-place over an orphan only produces a failed unit. Waiting on the pid matters for the same
122
+ reason.
123
+
124
+ Retirement is last, and deliberately not part of `install()`. Switching mechanism has to uninstall the
125
+ old one, but the old entry is often what is currently running the panel: removing it during the install
126
+ would stop the panel β€” and this plugin β€” before the new entry was ever told to start. So the ladder
127
+ carries the other mechanisms' removal steps in the activation plan, and each provider's
128
+ `retireCommands()` is the no-`--now` form for the same reason. launchd has no unload-without-stopping,
129
+ so its retirement is only safe once the panel is down and the new entry is up.
130
+
131
+ A mechanism that cannot start anything now answers `activate() β†’ null`: a `Run` value and a `.desktop`
132
+ file are read by the shell or the session at login. Nothing is stopped then β€” leaving a working panel
133
+ up beats killing it for an entry that would not bring it back. A handover that could not be spawned is
134
+ reported with the exact commands, and the panel is still up.
135
+
136
+ Enabling and switching differ only in the retirement: a switch already has an entry keeping the panel
137
+ alive, so its plan carries the old mechanism's removal.
138
+
139
+ ## Privilege
140
+
141
+ Boot scope needs privilege somewhere on every platform: a system unit, or `loginctl enable-linger` on
142
+ Linux; a LaunchDaemon on macOS; an elevated task on Windows. Login scope needs none. When this process
143
+ cannot elevate, the page shows the exact commands instead of prompting for a password β€” a headless
144
+ panel has no askpass and `sudo -n` is the only honest probe.
145
+
146
+ That refusal is shown with the status it answered, and the page retires it as soon as the status moves:
147
+ a person runs the commands and presses Re-check. The persisted last attempt follows the same rule, so a
148
+ failed install is not re-shown once an entry exists.
149
+
150
+ Switching those entries to a `User=` later is not just a unit edit. The moment a panel runs as root,
151
+ everything it writes β€” `<home>/.hh`, its workspaces, logs and `snapshots.json` β€” is root-owned, so the
152
+ unprivileged account the unit now names cannot write its own state and the panel fails to boot with a
153
+ permission error that never mentions ownership. Moving an install from root to a user therefore needs
154
+ `chown -R <user> <home>` (and the plugin state dir) first.