@cocorograph/hub-agent 0.8.69 → 0.8.70

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 (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/install.ps1 +93 -33
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.69",
3
+ "version": "0.8.70",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -103,10 +103,49 @@ function Get-LinuxUserName {
103
103
  return $u
104
104
  }
105
105
 
106
+ # Quote a value as ONE PowerShell single-quoted literal, for text that PowerShell
107
+ # itself will parse later (the RunOnce command line below). PowerShell escapes a quote
108
+ # by doubling it: o'hare -> 'o''hare'.
109
+ #
110
+ # WARNING: this is PowerShell quoting. It is NOT bash quoting, and there is deliberately
111
+ # no bash equivalent in this file any more -- everything handed to wsl.exe goes as plain
112
+ # argv tokens instead (see Invoke-WslInstaller).
113
+ #
114
+ # Doubling the ASCII quote is NOT enough on its own. PowerShell also accepts the
115
+ # typographic quotes as string delimiters, so a value containing U+2019 closes the
116
+ # literal early and the rest of it is parsed as code. Measured 2026-09-10 on pwsh:
117
+ # "'" + "x<U+2019>; Write-Output INJECTED; #" + "'" -> Invoke-Expression ran INJECTED.
118
+ # A double quote is just as bad one level out: it ends the -Command "..." region no
119
+ # matter how the inner value is quoted.
120
+ #
121
+ # So this allows printable ASCII only, minus the double quote, and refuses the rest.
122
+ # Enumerating dangerous characters is the losing side of this; an allowlist is not.
123
+ # An enrollment token and a hub URL are both ASCII, so nothing legitimate is lost.
124
+ #
125
+ # WARNING: the end anchor must be \z, not $. In .NET, $ also matches immediately before a
126
+ # final newline, so '...*$' accepts "tok" plus a trailing LF -- and a token pasted out of
127
+ # a browser or a text file carries exactly that. Measured 2026-09-10 on pwsh:
128
+ # ("tok" + [char]10) -match '^[\x20\x21\x23-\x7e]*$' -> True (leaks through)
129
+ # ("tok" + [char]10) -match '^[\x20\x21\x23-\x7e]*\z' -> False (rejected)
130
+ # The empty string still passes, which is what a run without a token needs.
131
+ #
132
+ # WARNING: the rejection must not echo the value. It carries the enrollment token, and
133
+ # an exception goes to the console, to transcripts and into whatever diagnostics get
134
+ # shared afterwards. Name the setting, not its contents.
135
+ function ConvertTo-PowerShellSingleQuoted($name, $value) {
136
+ $s = [string]$value
137
+ if ($s -notmatch '^[\x20\x21\x23-\x7e]*\z') {
138
+ throw "$name contains a character that cannot be safely embedded in the resume command (allowed: printable ASCII except the double quote)"
139
+ }
140
+ return "'" + ($s -replace "'", "''") + "'"
141
+ }
142
+
106
143
  # Register "re-run myself with RESUME=1 after reboot", carrying token/url.
107
144
  function Set-ResumeOnReboot {
145
+ $qTok = ConvertTo-PowerShellSingleQuoted "HUB_AGENT_TOKEN" $Token
146
+ $qUrl = ConvertTo-PowerShellSingleQuoted "HUB_AGENT_URL" $HubUrl
108
147
  $cmd = "powershell -NoProfile -ExecutionPolicy Bypass -Command " +
109
- "`"`$env:HUB_AGENT_TOKEN='$Token'; `$env:HUB_AGENT_URL='$HubUrl'; " +
148
+ "`"`$env:HUB_AGENT_TOKEN=$qTok; `$env:HUB_AGENT_URL=$qUrl; " +
110
149
  "`$env:HUB_AGENT_RESUME='1'; irm $PS1_URL | iex`""
111
150
  Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" `
112
151
  -Name $RUNONCE_NAME -Value $cmd
@@ -266,44 +305,65 @@ function Register-BootTask($distro, $user) {
266
305
  Write-Ok "Logon task '$TASK_NAME' registered (keeps WSL distro alive)"
267
306
  }
268
307
 
269
- # Quote a value as ONE POSIX-shell word, so a quote inside it cannot end the
270
- # quoting and turn the rest of the value into shell syntax. `'` becomes `'\''`
271
- # (close, escaped quote, reopen): o'hare -> 'o'\''hare'.
272
- #
273
- # WARNING: this is bash quoting only. Do NOT reuse it for the PowerShell that
274
- # Register-BootTask generates -- that text is parsed by PowerShell, not by a shell,
275
- # and these escapes would be passed through literally.
276
- function ConvertTo-BashSingleQuoted($value) {
277
- return "'" + (([string]$value) -replace "'", "'\''") + "'"
278
- }
279
-
280
308
  # Run the existing install.sh inside WSL.
281
309
  #
282
- # NOTE: do NOT use `curl ... | bash` here. A pipeline's exit status is the LAST
283
- # command's, so curl failing (DNS, 404, proxy) still leaves $LASTEXITCODE at 0 and
284
- # the guard below can never fire. That is exactly how this stayed broken: every run
285
- # printed "[OK] Setup complete" while install.sh had not run at all (2026-09-09).
286
- # Downloading to a file first makes curl's failure propagate through `&&`, and makes
287
- # $LASTEXITCODE install.sh's own exit code. It also keeps install.sh's stdin free
288
- # instead of handing it the script text.
310
+ # WARNING: do not build a shell expression and hand it to wsl.exe. 0.8.69 shipped
311
+ # bash -lc 't=$(mktemp) || exit 1; trap ...; curl -fsSL <url> -o "$t" && ... bash "$t"'
312
+ # and on a real Windows 11 host install.sh never ran: curl died with
313
+ # `option -o: is badly used here` (2026-09-10).
314
+ #
315
+ # What is measured, and what is not -- keep these apart, the inference below is what
316
+ # sent the first investigation into install.sh, which had not even been downloaded:
317
+ # - measured: curl 8.5.0 emits that message only when -o gets an empty *quoted*
318
+ # argument. So by the time bash ran curl, $t was empty.
319
+ # - measured: in the same failing run, the other `bash -lc` calls in this file
320
+ # (Set-Systemd, which contains && and ||) went through without error. The only
321
+ # thing that set the broken call apart was DOUBLE QUOTES inside the argument.
322
+ # - INFERRED, not confirmed: that the assignment got separated from the curl.
323
+ # - NOT measured: which layer re-quoted it. Windows PowerShell 5.1 and wsl.exe both
324
+ # re-quote native arguments and neither can be reproduced on a non-Windows test bed.
325
+ #
326
+ # So do not rely on knowing the rule -- rely on not needing one. One wsl.exe call per
327
+ # step, every argument a plain token, $LASTEXITCODE checked after each. Splitting the
328
+ # steps is also what makes failures propagate at all: the original `curl ... | bash`
329
+ # reported the pipeline's last exit status, so a failed download still left
330
+ # $LASTEXITCODE at 0 and every broken run printed "[OK] Setup complete" (2026-09-09).
289
331
  #
290
- # The EXIT trap removes the temp file when the run is interrupted. That one trap is
291
- # enough: a bash waiting on a foreground job handles SIGTERM after that job returns,
292
- # and exits through the normal path, so the EXIT trap runs and the exit code stays
293
- # non-zero. WARNING: do NOT add `trap ... TERM` on top of it. A TERM trap that only
294
- # cleans up and returns makes bash exit 0, i.e. a killed run would be reported as
295
- # success -- the very failure mode fixed above (both measured 2026-09-10, bash 5.2.21).
332
+ # Each throw names its own step, so a download failure cannot be read as install.sh
333
+ # failing. That confusion cost a release.
296
334
  function Invoke-WslInstaller($distro, $user) {
297
335
  Write-Step "Setting up hub-agent inside WSL (existing install.sh)"
298
336
  $tok = if ($Token) { $Token } else { "" }
299
- # Single-quoted here so $t stays literal for bash; '' is a literal quote.
300
- # `|| exit 1` covers mktemp failing before there is anything to clean up.
301
- $fmt = 't=$(mktemp) || exit 1; trap ''rm -f "$t"'' EXIT; curl -fsSL {0} -o "$t" && HUB_AGENT_TOKEN={1} HUB_AGENT_URL={2} bash "$t"'
302
- $inner = $fmt -f (ConvertTo-BashSingleQuoted $INSTALL_SH),
303
- (ConvertTo-BashSingleQuoted $tok),
304
- (ConvertTo-BashSingleQuoted $HubUrl)
305
- wsl.exe -d $distro -u $user -- bash -lc $inner
306
- if ($LASTEXITCODE -ne 0) { throw "install.sh inside WSL failed (exit $LASTEXITCODE)" }
337
+
338
+ # Ask the distro where HOME is instead of assuming /home/<user>. Download into HOME,
339
+ # not /tmp: a name under a world-writable directory can be pre-created by any other
340
+ # local account, and we are about to execute whatever lands there.
341
+ $wslHome = (wsl.exe -d $distro -u $user -- printenv HOME | Out-String)
342
+ if ($LASTEXITCODE -ne 0) { throw "could not resolve HOME for '$user' inside WSL (exit $LASTEXITCODE)" }
343
+ $wslHome = ($wslHome -replace "`r", "").Trim()
344
+ if (-not $wslHome) { throw "could not resolve HOME for '$user' inside WSL (empty)" }
345
+
346
+ # One name per run. With a fixed name, two concurrent runs corrupt each other: one
347
+ # curl truncates the file the other is already executing (bash reads a script as it
348
+ # goes, so the loser runs half an installer), and whichever finishes first deletes
349
+ # the other's script. HOME is not world-writable, so the only possible collision is
350
+ # with ourselves -- a GUID settles it without another round trip for mktemp.
351
+ $script = "$wslHome/.hub-agent-install." + [guid]::NewGuid().ToString("N") + ".sh"
352
+
353
+ try {
354
+ wsl.exe -d $distro -u $user -- curl -fsSL $INSTALL_SH -o $script
355
+ if ($LASTEXITCODE -ne 0) { throw "downloading install.sh inside WSL failed (exit $LASTEXITCODE)" }
356
+
357
+ # `env` carries the settings as plain argv tokens. Interpolating them into a shell
358
+ # string is what broke above, and argv also keeps the token out of shell history.
359
+ wsl.exe -d $distro -u $user -- env "HUB_AGENT_TOKEN=$tok" "HUB_AGENT_URL=$HubUrl" bash $script
360
+ if ($LASTEXITCODE -ne 0) { throw "install.sh inside WSL failed (exit $LASTEXITCODE)" }
361
+ } finally {
362
+ # Inside try so a partial download is reclaimed too. Best effort: a run killed from
363
+ # outside PowerShell leaves one .hub-agent-install.<guid>.sh in the user's own home.
364
+ # Never delete by wildcard here -- that would collect a concurrent run's script.
365
+ wsl.exe -d $distro -u $user -- rm -f $script | Out-Null
366
+ }
307
367
  }
308
368
 
309
369
  # =============================================================================