@twinsearth/w2m-dsh-plugin 0.6.3 → 0.8.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +1030 -0
  2. package/README.md +9 -2
  3. package/bin/w2m-localside.mjs +0 -0
  4. package/bin/w2m-rabbit.mjs +6 -0
  5. package/bin/w2m-stun.mjs +0 -0
  6. package/deploy/server/README.md +109 -0
  7. package/deploy/server/deploy-w2m.sh +161 -0
  8. package/deploy/server/w2m-localside.service +31 -0
  9. package/deploy/server/w2m-rabbit.service +26 -0
  10. package/deploy/server/w2m-stun.service +26 -0
  11. package/docs/ACCOUNT-FLEET-DESIGN.md +369 -0
  12. package/package.json +33 -22
  13. package/src/chatops/account.mjs +361 -0
  14. package/src/chatops/actions.mjs +227 -0
  15. package/src/chatops/approval-broker.mjs +320 -0
  16. package/src/chatops/auth.mjs +376 -0
  17. package/src/chatops/bridge.mjs +344 -0
  18. package/src/chatops/channel.mjs +192 -0
  19. package/src/chatops/commands.mjs +409 -0
  20. package/src/chatops/config-route.mjs +332 -0
  21. package/src/chatops/device-signing.mjs +218 -0
  22. package/src/chatops/durability.mjs +202 -0
  23. package/src/chatops/file-safety.mjs +282 -0
  24. package/src/chatops/fleet-scope.mjs +176 -0
  25. package/src/chatops/fleet-wiring.mjs +199 -0
  26. package/src/chatops/fleet.mjs +272 -0
  27. package/src/chatops/handoff.mjs +258 -0
  28. package/src/chatops/ilink-protocol.mjs +165 -0
  29. package/src/chatops/ilink-transport.mjs +168 -0
  30. package/src/chatops/ilink.mjs +423 -0
  31. package/src/chatops/index.mjs +429 -0
  32. package/src/chatops/outbound-queue.mjs +282 -0
  33. package/src/chatops/policy.mjs +356 -0
  34. package/src/chatops/run-command.mjs +308 -0
  35. package/src/plugin/tools.mjs +1 -1
  36. package/src/relay/server.mjs +96 -0
  37. package/src/relay/state.mjs +2233 -2011
package/README.md CHANGED
@@ -250,7 +250,7 @@ dsh plugin --profile <profile-name> add @twinsearth/w2m-dsh-plugin
250
250
 
251
251
  ```bash
252
252
  dsh plugin --profile <profile-name> add \
253
- https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.9/twinsearth-w2m-dsh-plugin-0.5.9.tgz
253
+ https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.6.3/twinsearth-w2m-dsh-plugin-0.6.3.tgz
254
254
  ```
255
255
 
256
256
  **From the repository** — the form a marketplace's one-click installer uses (it clones
@@ -624,6 +624,13 @@ move with it: `claims` is 19 tests here and **22 when `dist/` holds a packed tar
624
624
  its two tarball-vs-`package.json` checks run instead of skipping — so a checkout with `dist/` measures
625
625
  **1,142 tests / 1,134 pass / 4 skip**, and that is the number to expect from a release tree.
626
626
 
627
+ > **These counts are the v0.5.9 measurement and have not been re-taken since.** They are kept, dated
628
+ > and attributed rather than refreshed, because a figure presented as current without having been
629
+ > measured is the failure this project's own rules exist to prevent. Suites added after v0.5.9 —
630
+ > `p2p-crypto`, and from v0.7.0 `chatops-approval` and `chatops-policy` — are therefore **not** in
631
+ > any total on this page. Treat every count below as a v0.5.9 measurement until a release re-runs
632
+ > the suite on its own host and says so.
633
+
627
634
  **Which script runs which suite matters, because they are not the same set:**
628
635
 
629
636
  | Command | Suites | Result on this host (v0.5.9) |
@@ -810,7 +817,7 @@ node bin/w2m-localside.mjs --rabbit http://<中继地址>:8787 --pair PAIR-XXXXX
810
817
 
811
818
  # 3) 装 DSH 插件
812
819
  dsh plugin --profile <profile-name> add \
813
- https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.9/twinsearth-w2m-dsh-plugin-0.5.9.tgz
820
+ https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.6.3/twinsearth-w2m-dsh-plugin-0.6.3.tgz
814
821
  ```
815
822
 
816
823
  然后在 profile 的 `cordis.patch.yml` 里给插件 `rabbitUrl`(留空即用共享服务器)与 `operatorToken`,重启 DSH,即可直接用自然语言指挥:
File without changes
@@ -53,6 +53,9 @@ Credentials and state:
53
53
  --no-persist Keep everything in memory; forget devices and tasks on
54
54
  restart.
55
55
  --pairing-code <c> Use a fixed pairing code instead of a generated one.
56
+ --fleet <id> Bind this relay to one account's fleet. Omitted, the relay serves
57
+ every device (the pre-v0.7.7 behaviour). Set, a device with no
58
+ fleet reaches nothing and a device in another fleet is invisible.
56
59
 
57
60
  Request signing (v0.3.0):
58
61
  --signing-secret <s> Shared secret for HMAC-signed requests. Unset means the relay does
@@ -98,6 +101,9 @@ try {
98
101
  port: { type: 'string' },
99
102
  state: { type: 'string' },
100
103
  'pairing-code': { type: 'string' },
104
+ // v0.7.7: bind this relay to a fleet. Omitted means every device, which is the
105
+ // pre-v0.7.7 behaviour and therefore the migration-safe default.
106
+ fleet: { type: 'string' },
101
107
  'heartbeat-ms': { type: 'string' },
102
108
  'grace-ms': { type: 'string' },
103
109
  'base-path': { type: 'string' },
package/bin/w2m-stun.mjs CHANGED
File without changes
@@ -0,0 +1,109 @@
1
+ # Running the relay on a server
2
+
3
+ These are the units the shared relay **actually runs**, copied from
4
+ `/etc/systemd/system/` on the live host rather than written from the documentation. They are in the
5
+ repository because "how the server runs" is part of the thing being open-sourced — and because a
6
+ unit that exists only on one machine is a unit nobody can review, reproduce, or audit.
7
+
8
+ ## The three services
9
+
10
+ | Unit | What it is | Port |
11
+ |---|---|---|
12
+ | `w2m-rabbit.service` | The relay: P2P rendezvous, signalling, and the task ledger | 8787 |
13
+ | `w2m-stun.service` | STUN responder, for reflexive address discovery during hole punching | 3478 |
14
+ | `w2m-localside.service` | A Localside agent, so the server is itself a fleet machine | — |
15
+
16
+ `w2m-localside` is deliberately the same agent every other machine runs. The server is not
17
+ privileged in the fleet's protocol — it is a peer that happens to be reachable — and running it
18
+ through the normal agent is what keeps that true.
19
+
20
+ ## ⚠️ What is NOT here, and why
21
+
22
+ **`/var/lib/w2m/rabbit/` is not in this repository and must never be.**
23
+
24
+ It holds:
25
+
26
+ | File | Why it must not be published |
27
+ |---|---|
28
+ | `operator-token.txt` | **Full control of the fleet.** Anyone holding it can dispatch any allowed command to every machine. |
29
+ | `ledger` | The task history: every command run, on which machine, with which results. |
30
+
31
+ Two mechanisms keep this true, because one is not enough:
32
+
33
+ 1. **`.gitignore`** refuses the paths, so an accidental `git add -A` cannot stage them.
34
+ 2. **`scripts/scan-for-secrets.mjs`** reads the **content** of every tracked file and fails the
35
+ release on a credential shape. A `.gitignore` refuses a *path*; the ways a credential actually
36
+ escapes are pasting one into a source file, a fixture, or an example — so tracked content is
37
+ what gets read.
38
+
39
+ Run it before a release:
40
+
41
+ ```bash
42
+ node scripts/scan-for-secrets.mjs .
43
+ # 0 blocking, N for review -> exit 0
44
+ ```
45
+
46
+ `REVIEW` findings (long hex strings, high-entropy assignments) are **not fatal by design**: a
47
+ scanner whose false positives block releases is a scanner someone deletes. Only real credential
48
+ shapes stop anything.
49
+
50
+ ## Installing
51
+
52
+ ```bash
53
+ # 1. A user that owns the state and nothing else.
54
+ sudo useradd --system --home /var/lib/w2m --shell /usr/sbin/nologin w2m
55
+
56
+ # 2. The paths the units reference.
57
+ sudo install -d -o w2m -g w2m /opt/w2m /var/lib/w2m /var/log/w2m
58
+
59
+ # 3. The code, at the tag being deployed.
60
+ sudo -u w2m git clone https://github.com/TwinsEarth/dsh-windows2macos /opt/w2m/app
61
+ sudo -u w2m git -C /opt/w2m/app checkout v0.7.10
62
+
63
+ # 4. The units.
64
+ sudo cp deploy/server/*.service /etc/systemd/system/
65
+ sudo systemctl daemon-reload
66
+ sudo systemctl enable --now w2m-rabbit w2m-stun
67
+
68
+ # 5. Pair the server as a fleet machine, then enable the agent.
69
+ sudo systemctl enable --now w2m-localside
70
+ ```
71
+
72
+ ## Notes on the hardening, since it is easy to weaken by accident
73
+
74
+ Every unit sets `NoNewPrivileges`, `PrivateTmp`, `ProtectHome`, and a `ProtectSystem` level, and
75
+ writes only inside `/var/lib/w2m` via `ReadWritePaths`. That last one matters: `ProtectSystem=full`
76
+ makes `/usr` and `/boot` read-only, so an agent that tried to write anywhere else would fail
77
+ loudly rather than silently succeed on a machine that should not be writable.
78
+
79
+ `w2m-stun` is stricter (`ProtectSystem=strict`, `RestrictAddressFamilies=AF_INET AF_INET6`) because
80
+ it needs a socket and nothing else.
81
+
82
+ **If you add a capability to an agent, check `ReadWritePaths` first.** The failure is a permission
83
+ error at runtime, which looks like a bug in the agent rather than a hardening setting.
84
+
85
+ ## The `--fleet` flag, and why it is absent here
86
+
87
+ `w2m-rabbit` accepts `--fleet <account code>`, which binds the relay to one account's fleet. It is
88
+ **absent from this unit on purpose**, and the relay therefore runs unscoped: every paired device
89
+ reaches every other.
90
+
91
+ Turning it on is not a configuration change — it is a **fleet migration**:
92
+
93
+ 1. the account code must be read on a machine that is **signed in** (a headless server cannot
94
+ produce one; it has no DeepSeek account),
95
+ 2. the relay restarts scoped,
96
+ 3. **every device must be re-paired**, because a device receives its fleet *from the pairing code*,
97
+ and on a scoped relay a device with no fleet reaches **nothing**.
98
+
99
+ Step 3 is the risk: the fleet goes silent until each machine is re-paired. That is the intended
100
+ fail-safe direction and it is still an outage while it lasts.
101
+
102
+ ## Deliberately not included
103
+
104
+ * **TLS termination.** The live relay is behind a proxy that handles it; `--base-path` and
105
+ `--trust-proxy` exist for that arrangement and the certificate is the operator's business.
106
+ * **Backups.** The state directory is small (about 1 MB) and is the operator's to schedule. The
107
+ deployment script below does back it up before a swap, which is not the same as a backup policy.
108
+ * **Log rotation.** `StandardOutput=append:` grows without bound; the live host rotates
109
+ `/var/log/w2m/*.log` outside these units.
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Deploy one release tarball to a running relay, with a rollback that is automatic rather than
4
+ # remembered.
5
+ #
6
+ # This is the script the live server was updated with (v0.6.3 -> v0.7.7 -> v0.7.10). It is in the
7
+ # repository because a deployment that exists only in someone's shell history is a deployment nobody
8
+ # can review — and because the order of these steps is the whole safety argument:
9
+ #
10
+ # 1. verify the checksum BEFORE unpacking, so a truncated upload cannot become a running service
11
+ # 2. back up the tree in place, so rollback does not depend on the network
12
+ # 3. stage and check for entry points BEFORE swapping, so a bad tarball never reaches the
13
+ # live path
14
+ # 4. restart and health-check, and roll back automatically if the health check fails
15
+ #
16
+ # Step 3 is the one that matters most: `git archive` of a wrong subdirectory produces a tarball that
17
+ # unpacks cleanly and contains no `bin/`, and without the entry-point check that becomes a service
18
+ # that starts, finds nothing to run, and reports a confusing error.
19
+ #
20
+ # Usage: deploy-w2m.sh <tarball> <expected-sha256> [--fleet <account-code>]
21
+ #
22
+ set -euo pipefail
23
+
24
+ APP_DIR=/opt/w2m/app
25
+ BACKUP_ROOT=/opt/w2m
26
+ STATE_DIR=/var/lib/w2m
27
+ SERVICES=(w2m-rabbit w2m-stun w2m-localside)
28
+ STAMP=$(date -u +%Y%m%dT%H%M%SZ)
29
+
30
+ TARBALL="${1:-}"
31
+ EXPECTED="${2:-}"
32
+ FLEET=""
33
+ if [ "${3:-}" = "--fleet" ]; then FLEET="${4:-}"; fi
34
+
35
+ if [ -z "$TARBALL" ] || [ -z "$EXPECTED" ]; then
36
+ echo "usage: $0 <tarball> <expected-sha256> [--fleet <account-code>]" >&2
37
+ exit 2
38
+ fi
39
+ if [ ! -f "$TARBALL" ]; then echo "no such tarball: $TARBALL" >&2; exit 2; fi
40
+
41
+ echo "=== 1. verify the artefact ==="
42
+ ACTUAL=$(sha256sum "$TARBALL" | awk '{print $1}')
43
+ echo " expected $EXPECTED"
44
+ echo " actual $ACTUAL"
45
+ if [ "$ACTUAL" != "$EXPECTED" ]; then
46
+ echo " CHECKSUM MISMATCH -- refusing to deploy. A truncated upload must never become a service." >&2
47
+ exit 1
48
+ fi
49
+ echo " checksum OK"
50
+
51
+ echo
52
+ echo "=== 2. what is running now ==="
53
+ if [ -f "$APP_DIR/package.json" ]; then
54
+ echo " $APP_DIR is version $(node -e "process.stdout.write(require('$APP_DIR/package.json').version)")"
55
+ else
56
+ echo " $APP_DIR has no package.json (first deploy?)"
57
+ fi
58
+ for s in "${SERVICES[@]}"; do
59
+ printf ' %-16s %s\n' "$s" "$(systemctl is-active "$s" 2>/dev/null || echo inactive)"
60
+ done
61
+
62
+ echo
63
+ echo "=== 3. back up the current tree ==="
64
+ BACKUP="$BACKUP_ROOT/backup-$STAMP"
65
+ if [ -d "$APP_DIR" ]; then
66
+ cp -a "$APP_DIR" "$BACKUP"
67
+ echo " $APP_DIR -> $BACKUP"
68
+ else
69
+ echo " nothing to back up"
70
+ fi
71
+
72
+ echo
73
+ echo "=== 4. unpack to staging and check it ==="
74
+ STAGING=$(mktemp -d)
75
+ trap 'rm -rf "$STAGING"' EXIT
76
+ tar -xzf "$TARBALL" -C "$STAGING"
77
+ # The tarball contains a single `package/` root, by npm convention.
78
+ SRC="$STAGING/package"
79
+ if [ ! -d "$SRC" ]; then SRC="$STAGING"; fi
80
+
81
+ STAGED_VERSION=$(node -e "process.stdout.write(require('$SRC/package.json').version)" 2>/dev/null || echo "?")
82
+ echo " staged version: $STAGED_VERSION"
83
+
84
+ # The check that turns a confusing runtime failure into a clear deployment failure.
85
+ MISSING=""
86
+ for f in bin/w2m-rabbit.mjs bin/w2m-stun.mjs bin/w2m-localside.mjs package.json; do
87
+ [ -e "$SRC/$f" ] || MISSING="$MISSING $f"
88
+ done
89
+ if [ -n "$MISSING" ]; then
90
+ echo " MISSING ENTRY POINTS:$MISSING" >&2
91
+ echo " refusing to swap: this tarball would install a service that cannot start." >&2
92
+ exit 1
93
+ fi
94
+ echo " entry points present"
95
+
96
+ echo
97
+ echo "=== 5. swap into place ==="
98
+ rm -rf "$APP_DIR.old"
99
+ if [ -d "$APP_DIR" ]; then mv "$APP_DIR" "$APP_DIR.old"; fi
100
+ cp -a "$SRC" "$APP_DIR"
101
+ chown -R w2m:w2m "$APP_DIR" 2>/dev/null || true
102
+ echo " swapped; previous tree kept at $APP_DIR.old"
103
+
104
+ echo
105
+ echo "=== 6. restart ==="
106
+ # The relay's fleet binding: omitted means unscoped, which is the pre-v0.7.8 behaviour.
107
+ if [ -n "$FLEET" ]; then
108
+ echo " --fleet $FLEET requested"
109
+ if grep -q -- '--fleet' /etc/systemd/system/w2m-rabbit.service; then
110
+ sed -i "s|--fleet [^ ]*|--fleet $FLEET|" /etc/systemd/system/w2m-rabbit.service
111
+ else
112
+ sed -i "s|\(--state [^ ]*\)|\1 --fleet $FLEET|" /etc/systemd/system/w2m-rabbit.service
113
+ fi
114
+ systemctl daemon-reload
115
+ echo " WARNING: the relay is now scoped. A device with no fleet reaches NOTHING."
116
+ echo " Every device must be re-paired with a code issued by this relay."
117
+ fi
118
+ systemctl restart "${SERVICES[@]}"
119
+
120
+ echo
121
+ echo "=== 7. health check ==="
122
+ sleep 4
123
+ HEALTHY=1
124
+ for s in "${SERVICES[@]}"; do
125
+ STATE=$(systemctl is-active "$s" 2>/dev/null || echo inactive)
126
+ printf ' %-16s %s\n' "$s" "$STATE"
127
+ [ "$STATE" = "active" ] || HEALTHY=0
128
+ done
129
+
130
+ if command -v curl >/dev/null 2>&1; then
131
+ HEALTH=$(curl -fsS --max-time 10 http://127.0.0.1:8787/healthz 2>/dev/null || echo "")
132
+ if [ -n "$HEALTH" ]; then
133
+ echo " healthz: $HEALTH"
134
+ else
135
+ echo " healthz: NO ANSWER"
136
+ HEALTHY=0
137
+ fi
138
+ fi
139
+
140
+ if [ "$HEALTHY" != "1" ]; then
141
+ echo
142
+ echo "=== 8. ROLLBACK ==="
143
+ if [ -d "$BACKUP" ]; then
144
+ rm -rf "$APP_DIR"
145
+ mv "$BACKUP" "$APP_DIR"
146
+ systemctl restart "${SERVICES[@]}"
147
+ echo " rolled back to the previous tree and restarted" >&2
148
+ else
149
+ echo " no backup to roll back to" >&2
150
+ fi
151
+ exit 1
152
+ fi
153
+
154
+ echo
155
+ echo "=== deployed ==="
156
+ echo " version: $STAGED_VERSION"
157
+ echo " backup: $BACKUP"
158
+ echo " previous: $APP_DIR.old (removable once this is proven)"
159
+ echo
160
+ echo " NOTE: relay state lives in $STATE_DIR and was NOT touched by this script."
161
+ echo " The ledger and the operator token survive a deploy; that is deliberate."
@@ -0,0 +1,31 @@
1
+ [Unit]
2
+ Description=W2M Localside agent (shared server as a fleet machine)
3
+ After=network-online.target w2m-rabbit.service
4
+ Wants=network-online.target
5
+ Wants=w2m-rabbit.service
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=w2m
10
+ Group=w2m
11
+ WorkingDirectory=/opt/w2m/demo
12
+ ExecStart=/usr/local/bin/node /opt/w2m/app/bin/w2m-localside.mjs \
13
+ --rabbit http://127.0.0.1:8787 \
14
+ --project /opt/w2m/shared \
15
+ --name jp-shared \
16
+ --state /var/lib/w2m/localside-jp \
17
+ --p2p-mode auto \
18
+ --p2p-port 41234 \
19
+ --allowed-commands '["node --version","node --test","node -e","git status --porcelain","git rev-parse","git log","git diff","npm test","npm run"]'
20
+ Restart=always
21
+ RestartSec=3
22
+ NoNewPrivileges=true
23
+ PrivateTmp=true
24
+ ProtectSystem=full
25
+ ProtectHome=true
26
+ ReadWritePaths=/var/lib/w2m
27
+ StandardOutput=append:/var/log/w2m/localside.log
28
+ StandardError=append:/var/log/w2m/localside.log
29
+
30
+ [Install]
31
+ WantedBy=multi-user.target
@@ -0,0 +1,26 @@
1
+ [Unit]
2
+ Description=W2M Rabbit relay (P2P rendezvous, signalling and task ledger)
3
+ Documentation=https://github.com/TwinsEarth/dsh-windows2macos
4
+ After=network-online.target
5
+ Wants=network-online.target
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=w2m
10
+ Group=w2m
11
+ WorkingDirectory=/opt/w2m/app
12
+ ExecStart=/usr/local/bin/node /opt/w2m/app/bin/w2m-rabbit.mjs --host 0.0.0.0 --port 8787 --state /var/lib/w2m/rabbit
13
+ Restart=always
14
+ RestartSec=2
15
+ # The relay holds no model credentials and no working copy; it needs its state dir and
16
+ # nothing else. NoNewPrivileges + ProtectSystem close the obvious escalation paths.
17
+ NoNewPrivileges=true
18
+ PrivateTmp=true
19
+ ProtectSystem=full
20
+ ProtectHome=true
21
+ ReadWritePaths=/var/lib/w2m
22
+ StandardOutput=append:/var/log/w2m/rabbit.log
23
+ StandardError=append:/var/log/w2m/rabbit.log
24
+
25
+ [Install]
26
+ WantedBy=multi-user.target
@@ -0,0 +1,26 @@
1
+ [Unit]
2
+ Description=W2M STUN responder - reflexive address discovery for P2P hole punching
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ User=w2m
9
+ Group=w2m
10
+ ExecStart=/usr/local/bin/node /opt/w2m/app/bin/w2m-stun.mjs --host 0.0.0.0 --port 3478 --log-level info
11
+ Restart=always
12
+ RestartSec=2
13
+ StartLimitIntervalSec=60
14
+ StartLimitBurst=5
15
+ NoNewPrivileges=true
16
+ PrivateTmp=true
17
+ ProtectSystem=strict
18
+ ProtectHome=true
19
+ ProtectKernelTunables=true
20
+ ProtectControlGroups=true
21
+ RestrictAddressFamilies=AF_INET AF_INET6
22
+ StandardOutput=append:/var/log/w2m/stun.log
23
+ StandardError=append:/var/log/w2m/stun.log
24
+
25
+ [Install]
26
+ WantedBy=multi-user.target