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 +56 -33
- package/docs/AGENT-TOOLS.md +30 -0
- package/docs/BOOT.md +154 -0
- package/docs/DESIGN.md +31 -547
- package/docs/ENTRIES.md +117 -0
- package/docs/PANEL.md +107 -0
- package/docs/WORKSPACES.md +83 -0
- package/lib/client.js +8 -8
- package/lib/index.js +121 -27
- package/lib/index.js.map +4 -4
- package/lib/types/boot/types.d.ts +8 -2
- package/lib/types/service.d.ts +16 -2
- package/lib/types/shared/contracts.d.ts +10 -0
- package/lib/types/util/surface.d.ts +16 -0
- package/package.json +14 -13
package/README.md
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
# π dsh-home-hosted
|
|
4
4
|
|
|
5
|
-
**
|
|
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
|
-
|
|
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
|
[](https://www.npmjs.com/package/dsh-home-hosted)
|
|
13
16
|
[](https://github.com/NamesMT/dsh-home-hosted/actions/workflows/ci.yml)
|
|
14
17
|
[](./LICENSE)
|
|
15
18
|
[](https://nodejs.org)
|
|
16
|
-
[](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
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
β
|
|
55
|
-
β
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
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.
|