@twinsearth/w2m-dsh-plugin 0.5.8 → 0.6.1
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/CHANGELOG.md +384 -7
- package/CONTRIBUTING.md +14 -0
- package/PROTOCOL-v0.4.0.md +11 -3
- package/PROTOCOL.md +69 -15
- package/README.md +180 -27
- package/SECURITY.md +69 -19
- package/TROUBLESHOOTING.md +100 -53
- package/bin/w2m-rabbit.mjs +32 -5
- package/deploy/networking/LESSONS.md +18 -1
- package/deploy/networking/README.md +32 -7
- package/deploy/windows/README.md +20 -1
- package/deploy/windows/install-service.ps1 +98 -19
- package/deploy/windows/verify-supervisor.mjs +794 -0
- package/deploy/windows/w2m-agent-supervisor.mjs +867 -40
- package/docs/AUDIT.md +10 -1
- package/docs/DEPLOY.md +16 -3
- package/docs/P2P-CRYPTO.md +105 -4
- package/docs/PIPELINE-DESIGN.md +35 -10
- package/docs/TROUBLESHOOTING.md +22 -3
- package/package.json +6 -6
- package/src/agent/agent.mjs +152 -5
- package/src/agent/allowed-commands.mjs +539 -15
- package/src/agent/crypto.mjs +238 -40
- package/src/agent/exec.mjs +60 -4
- package/src/agent/p2p-node.mjs +40 -0
- package/src/agent/p2p.mjs +323 -30
- package/src/agent/resolve.mjs +68 -3
- package/src/plugin/fleet-target.mjs +160 -15
- package/src/plugin/tools.mjs +319 -56
- package/src/relay/server.mjs +74 -11
package/deploy/windows/README.md
CHANGED
|
@@ -28,7 +28,15 @@ It registers **one** task that runs `w2m-agent-supervisor.mjs`, which owns the a
|
|
|
28
28
|
|
|
29
29
|
* `-AtStartup` **and** `-AtLogOn`, `LogonType S4U` → runs whether the user is signed in or not,
|
|
30
30
|
in session 0, with no console for anything to close;
|
|
31
|
-
*
|
|
31
|
+
* a **watchdog trigger** every 5 minutes (`-WatchdogMinutes`), because a supervisor killed by a
|
|
32
|
+
console teardown cannot be restarted by `RestartCount` alone — the task simply ends;
|
|
33
|
+
* `MultipleInstances Parallel` → the trigger may start a second supervisor, which is safe because
|
|
34
|
+
the supervisor's own lock is what prevents a second *agent*: a generation that finds the lock held
|
|
35
|
+
logs a no-op line and exits without spawning. (This replaced `IgnoreNew`, which could not recover
|
|
36
|
+
from the one death that matters: measured, a task whose supervisor died while the task still
|
|
37
|
+
reported `Running` refused every later start with `0x800710E0` and stayed dead — one supervisor,
|
|
38
|
+
one agent, the port bound, and no way back. Re-register an existing installation with
|
|
39
|
+
`-UpdateTask`, which needs no elevation and touches only the triggers and settings.)
|
|
32
40
|
* `RestartCount 999` / 1 min, `ExecutionTimeLimit 0`, `-StartWhenAvailable`.
|
|
33
41
|
|
|
34
42
|
Everything except the task's own command line goes into **`%USERPROFILE%\.dsh\w2m\agent.json`**,
|
|
@@ -101,11 +109,22 @@ back to the relay with `offer_path: "relay"` in the ledger.
|
|
|
101
109
|
|
|
102
110
|
## Verify it, rather than trusting it
|
|
103
111
|
|
|
112
|
+
The supervisor's invariants — one generation, one agent, the punch port bound by that agent — have a
|
|
113
|
+
runnable check that needs no administrator right and cannot disturb a live installation (it drives
|
|
114
|
+
the supervisor against a stub worker on ports the OS picks):
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
node deploy\windows\verify-supervisor.mjs # 8 checks; --keep leaves the temp tree behind
|
|
118
|
+
```
|
|
119
|
+
|
|
104
120
|
```powershell
|
|
105
121
|
# the task is running, and in which session
|
|
106
122
|
Get-ScheduledTask -TaskName 'W2M Localside agent' | Select-Object State
|
|
107
123
|
Get-Process node | Where-Object { $_.SessionId -eq 0 } | Select-Object Id, SessionId, StartTime
|
|
108
124
|
|
|
125
|
+
# the supervisor's own account of itself: state, the live child, the reaped pids, the punch port
|
|
126
|
+
Get-Content "$env:USERPROFILE\.dsh\w2m\supervisor-heartbeat.json"
|
|
127
|
+
|
|
109
128
|
# the agent's own narration: "stream ready" is the line that means the fleet can reach it
|
|
110
129
|
Get-Content "$env:USERPROFILE\.dsh\w2m\agent.log" -Tail 20
|
|
111
130
|
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
# Get-ScheduledTask -TaskName 'W2M Localside agent' | Get-ScheduledTaskInfo
|
|
41
41
|
# powershell -ExecutionPolicy Bypass -File deploy\windows\install-service.ps1 -Uninstall
|
|
42
42
|
#
|
|
43
|
+
# # repair a task registered by an older version of this script (one that used
|
|
44
|
+
# # `MultipleInstances IgnoreNew` and therefore could not recover from a supervisor death):
|
|
45
|
+
# powershell -ExecutionPolicy Bypass -File deploy\windows\install-service.ps1 -UpdateTask
|
|
46
|
+
#
|
|
43
47
|
# Everything after the parameters goes to the agent, so `-AgentArgs
|
|
44
48
|
# '--p2p-port','41235'` pins the punch socket (and then one inbound firewall rule
|
|
45
49
|
# is enough, instead of a rule per restart).
|
|
@@ -58,6 +62,8 @@ param(
|
|
|
58
62
|
[int]$RestartBackoffMs = 60000,
|
|
59
63
|
[int]$WatchdogMinutes = 5,
|
|
60
64
|
[switch]$Interactive,
|
|
65
|
+
[switch]$UpdateTask,
|
|
66
|
+
[switch]$UpdateConfig,
|
|
61
67
|
[switch]$Uninstall,
|
|
62
68
|
[switch]$WhatIfOnly
|
|
63
69
|
)
|
|
@@ -65,6 +71,54 @@ param(
|
|
|
65
71
|
$ErrorActionPreference = 'Stop'
|
|
66
72
|
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) # ...\deploy\windows -> repo root
|
|
67
73
|
$supervisor = Join-Path $PSScriptRoot 'w2m-agent-supervisor.mjs'
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------------------------
|
|
76
|
+
# -UpdateTask: repair an EXISTING registration, and touch nothing else.
|
|
77
|
+
#
|
|
78
|
+
# This exists because the task and the supervisor have to agree, and an installation registered by an
|
|
79
|
+
# older version of this script does not: `MultipleInstances IgnoreNew` together with the watchdog
|
|
80
|
+
# trigger means a task whose supervisor died while the task still reports Running can never start
|
|
81
|
+
# again. Measured on this machine: one supervisor, one agent, the port bound, and no way back.
|
|
82
|
+
#
|
|
83
|
+
# It runs FIRST and RETURNS, before the agent config is written and before node is even looked for,
|
|
84
|
+
# so it needs no elevation: `Set-ScheduledTask` on a task this user already owns is permitted
|
|
85
|
+
# (measured), while re-registering the S4U principal is not. Triggers and settings only -- the action,
|
|
86
|
+
# the principal and `%USERPROFILE%\.dsh\w2m\agent.json` are left exactly as they are.
|
|
87
|
+
# ---------------------------------------------------------------------------------------------
|
|
88
|
+
if ($UpdateTask) {
|
|
89
|
+
if (-not (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue)) {
|
|
90
|
+
Write-Error "nothing to update: $TaskName is not registered. Run without -UpdateTask to install it."
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
$watchdog = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
|
|
94
|
+
-RepetitionInterval (New-TimeSpan -Minutes $WatchdogMinutes) `
|
|
95
|
+
-RepetitionDuration ([TimeSpan]::FromDays(3650))
|
|
96
|
+
$existingTriggers = (Get-ScheduledTask -TaskName $TaskName).Triggers
|
|
97
|
+
$isInteractive = -not ($existingTriggers | Where-Object { $_.CimClass.CimClassName -eq 'MSFT_TaskBootTrigger' })
|
|
98
|
+
$updateTriggers = if ($isInteractive) {
|
|
99
|
+
@((New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"), $watchdog)
|
|
100
|
+
} else {
|
|
101
|
+
@(
|
|
102
|
+
(New-ScheduledTaskTrigger -AtStartup),
|
|
103
|
+
(New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"),
|
|
104
|
+
$watchdog
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
$updateSettings = New-ScheduledTaskSettingsSet `
|
|
108
|
+
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
|
109
|
+
-MultipleInstances Parallel `
|
|
110
|
+
-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
|
|
111
|
+
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
|
|
112
|
+
-StartWhenAvailable -Hidden
|
|
113
|
+
Set-ScheduledTask -TaskName $TaskName -Trigger $updateTriggers -Settings $updateSettings | Out-Null
|
|
114
|
+
$updated = Get-ScheduledTask -TaskName $TaskName
|
|
115
|
+
Write-Host "updated $TaskName (triggers and settings only; the action and the config are untouched)"
|
|
116
|
+
Write-Host " multiple instances : $($updated.Settings.MultipleInstances) (Parallel: the supervisor's lock is what prevents a second agent)"
|
|
117
|
+
Write-Host " watchdog : every $WatchdogMinutes min, indefinitely"
|
|
118
|
+
Write-Host " state : $($updated.State)"
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
68
122
|
$node = (Get-Command node -ErrorAction SilentlyContinue).Source
|
|
69
123
|
if (-not $node) { throw 'node is not on PATH; install Node 20.19+ first' }
|
|
70
124
|
if (-not (Test-Path $supervisor)) { throw "supervisor missing: $supervisor" }
|
|
@@ -124,6 +178,27 @@ if ($WhatIfOnly) {
|
|
|
124
178
|
$config | ConvertTo-Json -Depth 4
|
|
125
179
|
return
|
|
126
180
|
}
|
|
181
|
+
# THE CONFIG IS THE PART THAT CANNOT BE REBUILT FROM DEFAULTS.
|
|
182
|
+
#
|
|
183
|
+
# Measured the hard way while testing this deployment: re-running the installer with no parameters
|
|
184
|
+
# overwrote a live `agent.json` with the defaults -- the project became the current directory, the
|
|
185
|
+
# machine name became the hostname, and `--p2p-mode auto --p2p-port 41235` disappeared, so the agent
|
|
186
|
+
# immediately took an ephemeral port and every dispatch would have fallen back to the relay. Nothing
|
|
187
|
+
# errored and nothing warned; the file was simply replaced. Refusing by default, and naming exactly
|
|
188
|
+
# what would be lost, is one line of protection against the failure this whole directory is about.
|
|
189
|
+
if ((Test-Path $configPath) -and -not $UpdateConfig -and -not $WhatIfOnly) {
|
|
190
|
+
$current = Get-Content $configPath -Raw | ConvertFrom-Json
|
|
191
|
+
if ($current.project -ne $Project -or $current.name -ne $MachineName -or [string]$current.agentArgs -ne ($agentArgs -join ' ')) {
|
|
192
|
+
Write-Error @"
|
|
193
|
+
$configPath already exists and differs from what this run would write:
|
|
194
|
+
now : project=$($current.project) name=$($current.name) agentArgs="$($current.agentArgs)"
|
|
195
|
+
next: project=$Project name=$MachineName agentArgs="$($agentArgs -join ' ')"
|
|
196
|
+
Re-run with -UpdateConfig to replace it, or with -WhatIfOnly to see the whole file first.
|
|
197
|
+
A silent replacement is how a machine loses its pinned punch port and its identity.
|
|
198
|
+
"@
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
}
|
|
127
202
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $configPath) | Out-Null
|
|
128
203
|
# Write it WITHOUT a BOM. `Set-Content -Encoding utf8` on PowerShell 5.1 prepends one, `JSON.parse`
|
|
129
204
|
# refuses it, and the supervisor then exits 78 before it has opened its log -- a failure with no
|
|
@@ -135,28 +210,30 @@ Write-Host "wrote $configPath"
|
|
|
135
210
|
|
|
136
211
|
$action = New-ScheduledTaskAction -Execute $node -Argument ($arguments -join ' ') -WorkingDirectory $PSScriptRoot
|
|
137
212
|
# ---------------------------------------------------------------------------------------------
|
|
138
|
-
#
|
|
213
|
+
# THE WATCHDOG TRIGGER, AND WHY THE TASK MAY NOW RUN MORE THAN ONE INSTANCE
|
|
139
214
|
#
|
|
140
215
|
# Measured twice on this machine: the whole process tree (supervisor + agent) is killed with
|
|
141
216
|
# 0xC000013A (STATUS_CONTROL_C_EXIT -- its console went away), the log ends mid-sentence, and Task
|
|
142
217
|
# Scheduler does NOT bring it back even with RestartCount 999: the task simply ends. The fleet then
|
|
143
|
-
# silently loses the machine, which is exactly the failure a supervisor was supposed to prevent
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
# `MultipleInstances IgnoreNew`
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
218
|
+
# silently loses the machine, which is exactly the failure a supervisor was supposed to prevent.
|
|
219
|
+
#
|
|
220
|
+
# A repeating trigger fixes that without any administrator right: the task means "be running, or
|
|
221
|
+
# start", so a death from any cause is repaired at the next tick. Until recently that trigger had to
|
|
222
|
+
# be paired with `MultipleInstances IgnoreNew` to keep it from producing a second agent, and that
|
|
223
|
+
# pairing is what cost this machine a day: measured, a supervisor killed while the task still
|
|
224
|
+
# reported Running left the task in `Running` forever (the rejected start is 0x800710E0, "the
|
|
225
|
+
# operator or administrator has refused the request"), so the watchdog could never fire again while
|
|
226
|
+
# the detached agent kept the punch port -- one supervisor, one agent, port bound, and no way back.
|
|
227
|
+
#
|
|
228
|
+
# `MultipleInstances Parallel` is now the correct setting, because the supervisor's own lock is what
|
|
229
|
+
# makes a second start harmless: a generation that finds the lock held logs a no-op line and exits
|
|
230
|
+
# without spawning, so a tick during a healthy run costs one short-lived process and nothing else.
|
|
231
|
+
# The trigger therefore does what it was written to do -- repair a dead supervisor -- and cannot do
|
|
232
|
+
# the thing it was feared for.
|
|
151
233
|
# ---------------------------------------------------------------------------------------------
|
|
152
|
-
$repetition =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# left with no agent at all). Ten years is the idiomatic stand-in for "repeat indefinitely".
|
|
156
|
-
New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
|
|
157
|
-
-RepetitionInterval (New-TimeSpan -Minutes $WatchdogMinutes) `
|
|
158
|
-
-RepetitionDuration ([TimeSpan]::FromDays(3650))
|
|
159
|
-
} else { $null }
|
|
234
|
+
$repetition = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
|
|
235
|
+
-RepetitionInterval (New-TimeSpan -Minutes $WatchdogMinutes) `
|
|
236
|
+
-RepetitionDuration ([TimeSpan]::FromDays(3650))
|
|
160
237
|
# AtStartup only makes sense for the session-0 (S4U) shape, which also runs whether or not anyone is
|
|
161
238
|
# signed in. Registering an AtStartup trigger on an interactive task is refused without elevation --
|
|
162
239
|
# measured, and the reason this list is conditional rather than always two entries.
|
|
@@ -165,15 +242,17 @@ $triggers = if ($Interactive) {
|
|
|
165
242
|
} else {
|
|
166
243
|
@(
|
|
167
244
|
(New-ScheduledTaskTrigger -AtStartup),
|
|
168
|
-
(New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME")
|
|
245
|
+
(New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"),
|
|
246
|
+
$repetition
|
|
169
247
|
)
|
|
170
248
|
}
|
|
171
249
|
$settings = New-ScheduledTaskSettingsSet `
|
|
172
250
|
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
|
173
|
-
-MultipleInstances
|
|
251
|
+
-MultipleInstances Parallel `
|
|
174
252
|
-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
|
|
175
253
|
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
|
|
176
254
|
-StartWhenAvailable -Hidden
|
|
255
|
+
|
|
177
256
|
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
|
|
178
257
|
-LogonType $(if ($Interactive) { 'Interactive' } else { 'S4U' }) -RunLevel Limited
|
|
179
258
|
|