@cocorograph/hub-agent 0.6.79 → 0.6.81

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.6.79",
3
+ "version": "0.6.81",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/main.mjs CHANGED
@@ -25,6 +25,7 @@ import { WsClient } from "./ws-client.mjs"
25
25
  import { PtyBridge } from "./pty-bridge.mjs"
26
26
  import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
27
27
  import { UploadManager } from "./claude-upload.mjs"
28
+ import { requestSelfUninstall } from "./service-install.mjs"
28
29
  import {
29
30
  decideSessionRotation,
30
31
  fetchSessionHistory,
@@ -656,6 +657,8 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
656
657
  }
657
658
  process.on("SIGINT", () => shutdown("SIGINT"))
658
659
  process.on("SIGTERM", () => shutdown("SIGTERM"))
660
+ // dispatch から呼べるように shutdown を ctx に載せる (agent.shutdown 用)。
661
+ ctx.requestShutdown = shutdown
659
662
 
660
663
  // 0.6.2 fix: 例外で silent 終了しないよう最後のセーフティネット。
661
664
  // Node 24 で unhandledRejection は default で process を kill する仕様のため、
@@ -1136,6 +1139,21 @@ async function dispatch(msg, ctx) {
1136
1139
  case "error":
1137
1140
  ctx.logger.warn({ msg }, "hub error")
1138
1141
  return
1142
+ case "agent.shutdown": {
1143
+ // Cockpit の「エージェント削除 (ゴミ箱)」由来。この端末をエージェントとして
1144
+ // 撤去する: サービス停止・無効化・autostart 撤去のみ。ユーザーデータ
1145
+ // (~/.hub / ~/.claude / プロジェクト) には触れない。
1146
+ ctx.logger.info({ msg }, "agent.shutdown received — self-uninstalling")
1147
+ try {
1148
+ // cgroup の外で動く detached プロセスにクリーンアップを委譲してから exit。
1149
+ requestSelfUninstall()
1150
+ } catch (err) {
1151
+ ctx.logger.warn({ err: err?.message }, "requestSelfUninstall failed")
1152
+ }
1153
+ // 本体は即停止 (detached cleanup が disable/stop を完走させる)。
1154
+ await ctx.requestShutdown?.("agent.shutdown")
1155
+ return
1156
+ }
1139
1157
  case "pty.attach": {
1140
1158
  const stream_id = msg.stream_id
1141
1159
  try {
@@ -19,6 +19,9 @@ import { readConfig } from "./config.mjs"
19
19
 
20
20
  const PLIST_LABEL = "co.cocorograph.hub-agent"
21
21
  const SYSTEMD_UNIT_NAME = "hub-agent.service"
22
+ // Windows(WSL) のログオン keep-alive タスク名 (install.ps1 の $TASK_NAME と一致)。
23
+ // WSL からは Windows 連携 (interop) で schtasks.exe を叩いて削除できる。
24
+ const WSL_BOOT_TASK_NAME = "HubAgentWSLBoot"
22
25
 
23
26
  function repoTemplatesDir() {
24
27
  // src/service-install.mjs → ../templates
@@ -323,7 +326,14 @@ export async function installService({ bin } = {}) {
323
326
  // 無い間 (= WSL2 のログオンタスク経由の boot や headless) に常駐しない。
324
327
  const linger = enableLinger()
325
328
  run("systemctl", ["--user", "daemon-reload"])
326
- run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT_NAME])
329
+ run("systemctl", ["--user", "enable", SYSTEMD_UNIT_NAME])
330
+ // `enable --now` は既に起動中のサービスを再起動しない (--now は停止中なら start、
331
+ // 起動中なら no-op)。distro を起動し続ける keep-alive と組み合わさると、再 enroll で
332
+ // agent.json のトークンを書き換えても古いプロセスが旧トークンのまま生き続け、
333
+ // (同一ホスト重複解消で旧 agent 行が消えるため) WS 403 でオフラインになる。
334
+ // restart は停止中なら起動・起動中なら再起動するので、install-service のたびに
335
+ // 必ず最新の agent.json でプロセスを入れ替える。
336
+ run("systemctl", ["--user", "restart", SYSTEMD_UNIT_NAME])
327
337
  return {
328
338
  platform: "linux",
329
339
  path: dest,
@@ -409,6 +419,75 @@ export async function uninstallService() {
409
419
  throw new Error(`unsupported platform: ${process.platform}`)
410
420
  }
411
421
 
422
+ /**
423
+ * Cockpit の「エージェント削除 (ゴミ箱)」を受けて、この端末を **エージェントとして
424
+ * 撤去** する。サービス停止・無効化・autostart 撤去のみを行い、ユーザーデータ
425
+ * (`~/.hub` / `~/.claude` / プロジェクト) には一切触れない。
426
+ *
427
+ * 重要: hub-agent.service は `Restart=always`。自プロセスから `systemctl --user
428
+ * stop` すると自分の cgroup ごと殺され、後続のクリーンアップが完走しない。そこで
429
+ * **サービスの cgroup の外** で動く detached プロセスにクリーンアップを委譲し、
430
+ * 本体はすぐ exit する (呼び出し側が exit する)。
431
+ *
432
+ * 撤去内容:
433
+ * - systemd user unit: disable --now + unit ファイル削除 + daemon-reload
434
+ * - linger: loginctl disable-linger
435
+ * - Windows(WSL): schtasks.exe で keep-alive タスク削除 (interop。非WSLでは no-op)
436
+ *
437
+ * @param {{spawnSync?: typeof spawnSync}} [opts] テスト用に spawn を注入可能。
438
+ * @returns {boolean} クリーンアップ用 detached プロセスの起動に成功したら true。
439
+ */
440
+ export function requestSelfUninstall(opts = {}) {
441
+ const spawn = opts.spawnSync ?? spawnSync
442
+
443
+ if (process.platform === "linux") {
444
+ const user = currentUsername()
445
+ const unit = linuxUnitPath()
446
+ // 1 秒待ってから (本体 exit を待つ) 撤去。各手順は失敗しても続行 (|| true)。
447
+ const script = [
448
+ "sleep 1",
449
+ `systemctl --user disable --now ${SYSTEMD_UNIT_NAME} 2>/dev/null || true`,
450
+ `rm -f '${unit}' 2>/dev/null || true`,
451
+ "systemctl --user daemon-reload 2>/dev/null || true",
452
+ `loginctl disable-linger ${user} 2>/dev/null || true`,
453
+ // WSL のみ有効 (Windows interop)。schtasks.exe が無い環境では黙って失敗。
454
+ `schtasks.exe /delete /tn ${WSL_BOOT_TASK_NAME} /f >/dev/null 2>&1 || true`,
455
+ ].join("; ")
456
+
457
+ // systemd-run --user --scope で別 scope(=別 cgroup) に逃がす。これにより
458
+ // hub-agent.service を stop しても掃除プロセスは生き残って完走する。
459
+ const r = spawn(
460
+ "systemd-run",
461
+ ["--user", "--scope", "--collect", "--quiet", "bash", "-lc", script],
462
+ { stdio: "ignore", detached: true },
463
+ )
464
+ if (!r.error && r.status === 0) return true
465
+ // systemd-run が無い/失敗時は setsid で cgroup から外して代替。
466
+ const r2 = spawn("setsid", ["bash", "-lc", script], {
467
+ stdio: "ignore",
468
+ detached: true,
469
+ })
470
+ return !r2.error
471
+ }
472
+
473
+ if (process.platform === "darwin") {
474
+ const uid = ensureUnixUid()
475
+ const dest = macPlistPath()
476
+ // launchctl bootout は job の全プロセスを殺すため、setsid で job から切り離した
477
+ // detached プロセスに委譲する。
478
+ const script =
479
+ `sleep 1; launchctl bootout gui/${uid} '${dest}' 2>/dev/null || true; ` +
480
+ `rm -f '${dest}' 2>/dev/null || true`
481
+ const r = spawn("bash", ["-lc", `setsid bash -lc "${script}" >/dev/null 2>&1 &`], {
482
+ stdio: "ignore",
483
+ detached: true,
484
+ })
485
+ return !r.error
486
+ }
487
+
488
+ return false
489
+ }
490
+
412
491
  export const _internal = {
413
492
  expandTemplate,
414
493
  detectHubAgentBin,
@@ -420,4 +499,5 @@ export const _internal = {
420
499
  bootstrapWithRetry,
421
500
  currentUsername,
422
501
  enableLinger,
502
+ requestSelfUninstall,
423
503
  }