cofluxd 0.5.0 → 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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/cofluxd.mjs +128 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -16,7 +16,8 @@ cofluxd up # 幂等:零参数即可装/起;已装则按当前
16
16
  cofluxd status # 服务器/登记(含"等待授权")/服务/连接状态
17
17
  cofluxd doctor # 中心网络 + gateway/grant/loopback + daemon 状态分层自检
18
18
  cofluxd logs -f # 看 daemon 日志
19
- cofluxd update # 更新本地 supervisor 二进制并重启(worker server 自动热升级)
19
+ cofluxd update # 下载新二进制(不重启;supervisor 有变化时提示用 restart 应用)
20
+ cofluxd restart # 重启 daemon 应用新 supervisor(⚠ 结束本机所有活会话)
20
21
  cofluxd down # 停止
21
22
  cofluxd uninstall [--purge] # 卸载(--purge 连二进制/配置/凭证一并删)
22
23
  ```
package/cofluxd.mjs CHANGED
@@ -115,6 +115,22 @@ function fdaLabel(status) {
115
115
 
116
116
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
117
117
 
118
+ function fileSha256(path) {
119
+ try { return crypto.createHash("sha256").update(fs.readFileSync(path)).digest("hex"); } catch { return null; }
120
+ }
121
+
122
+ // macOS:新落盘的二进制带 com.apple.provenance,launchd 顶层 spawn 会被 AMFI 以
123
+ // OS_REASON_CODESIGNING 静默杀死——即使产物是 Developer ID 签名 + 已公证(2026-07-25
124
+ // v0.13.0 实测仍被连杀)。本地 ad-hoc 重签使其成为"本机产物"绕开该检查;签名威胁模型
125
+ // 不受影响(防的是中心被攻破推恶意产物,靠下载后的 sha256+ed25519 验签,与此无关)。
126
+ function resignMacBinaries(paths) {
127
+ if (!IS_MAC) return;
128
+ for (const p of paths) {
129
+ const r = run("codesign", ["--force", "-s", "-", p]);
130
+ if (r.status !== 0) console.warn(`⚠ 本地重签失败: ${p}(daemon 若被系统杀,手动 codesign --force -s - 该文件后 cofluxd restart)`);
131
+ }
132
+ }
133
+
118
134
  async function download(url, dest) {
119
135
  const res = await fetch(url, { redirect: "follow" });
120
136
  if (!res.ok) die(`下载失败 HTTP ${res.status}: ${url}\n(该版本/平台的 release 资产是否已发布?)`);
@@ -145,6 +161,7 @@ async function ensureBinaries({ version, binDir, skipIfPresent }) {
145
161
  fs.copyFileSync(src, join(BIN_DIR, b));
146
162
  fs.chmodSync(join(BIN_DIR, b), 0o755);
147
163
  }
164
+ resignMacBinaries([SUP_BIN, WRK_BIN]);
148
165
  console.log(`✓ 用本地二进制(${binDir})`);
149
166
  return;
150
167
  }
@@ -166,6 +183,7 @@ async function ensureBinaries({ version, binDir, skipIfPresent }) {
166
183
  await download(`${base}/${b}-${t}`, join(BIN_DIR, b));
167
184
  console.log("✓");
168
185
  }
186
+ resignMacBinaries([SUP_BIN, WRK_BIN]);
169
187
  }
170
188
 
171
189
  // 写 settings.json(daemon 直接读)。
@@ -298,20 +316,41 @@ function cmdDown() { stopService(); console.log("✓ 已停止"); }
298
316
  // 只有 supervisor 真正靠这条命令更新——它不自动升级(持 PTY,重启会杀会话,见 plan 017)。
299
317
  // worker 运行中会被 server 自动热升级到最新 stable(无需停服);这里顺带刷新的是 worker 的
300
318
  // "内置兜底二进制"(supervisor 冷启动/热升级缓存皆无时的 fallback),日常可不管。
319
+ //
320
+ // update 与 restart 拆开(2026-07-25 用户拍板):update 只落盘二进制、绝不重启——替换磁盘
321
+ // 文件对运行中的 supervisor/PTY 零影响;supervisor 内容没变就明说不用重启(大多数发版
322
+ // 都不动 supervisor,旧行为等于白丢一次全部会话)。真正的中断收敛到显式的 `cofluxd restart`。
301
323
  async function cmdUpdate(v) {
302
324
  if (!fs.existsSync(SETTINGS)) die("尚未安装,先 cofluxd up");
303
325
  const version = v.version || "latest";
326
+ const before = fileSha256(SUP_BIN);
304
327
  await ensureBinaries({ version, binDir: v["bin-dir"] });
328
+ if (before === fileSha256(SUP_BIN)) {
329
+ console.log("✓ 已更新:supervisor 无变化——不需要重启,活会话不受影响");
330
+ console.log(" worker 由 server 自动热升级;本次刷新的只是兜底 worker 二进制。");
331
+ } else {
332
+ console.log(`✓ 新 supervisor 已就位(${v["bin-dir"] ? "本地产物" : version}),但尚未生效`);
333
+ console.log(" ⚠ 运行 `cofluxd restart` 应用——会重启 daemon 并结束本机所有活会话(PTY 随 supervisor 退出)。");
334
+ }
335
+ }
336
+
337
+ // 唯一会主动中断会话的命令:应用已就位的新 supervisor(或单纯重启 daemon)。
338
+ function cmdRestart() {
339
+ if (!fs.existsSync(SETTINGS)) die("尚未安装,先 cofluxd up");
340
+ console.log("⚠ 正在重启 daemon:本机所有活会话将结束(PTY 随 supervisor 退出)…");
305
341
  restartService();
306
- console.log(`✓ supervisor 已更新到 ${v["bin-dir"] ? "本地产物" : version} 并重启`);
307
- console.log(" worker 由 server 自动热升级,通常无需手动更新;此命令只更新 supervisor(及其兜底 worker 二进制)。");
342
+ console.log("✓ 已重启(cofluxd status 查看状态)");
308
343
  }
309
344
 
310
345
  // launchd(macOS) / systemd --user(Linux) 服务活跃态查询,status 与 doctor 共用。
346
+ // macOS 必须以 launchctl list 输出里的 PID 为准:label 登记 ≠ 进程存活(supervisor 被
347
+ // OS_REASON_CODESIGNING 杀掉时 label 仍在,旧实现会报假"运行中",2026-07-25 实测踩过)。
311
348
  function serviceRunningInfo() {
312
349
  if (IS_MAC) {
313
- const running = run("launchctl", ["list", "com.coflux.daemon"]).status === 0;
314
- return { running, active: running ? "运行中" : "未运行" };
350
+ const out = run("launchctl", ["list", "com.coflux.daemon"]);
351
+ const registered = out.status === 0;
352
+ const running = registered && /"PID"\s*=\s*\d+/.test(out.stdout || "");
353
+ return { running, active: running ? "运行中" : registered ? "已登记但进程不在(看 cofluxd logs;常见于系统拒签杀进程)" : "未运行" };
315
354
  }
316
355
  if (IS_LINUX) {
317
356
  const active = (run("systemctl", ["--user", "is-active", "coflux-daemon.service"]).stdout || "").trim() || "未运行";
@@ -625,17 +664,100 @@ function cmdUninstall(v) {
625
664
  else console.log(`✓ 已卸载服务(保留二进制/配置/凭证于 ${HOME};--purge 可全清)`);
626
665
  }
627
666
 
667
+ /* ------------------------------ hook:agent 事件信使 ------------------------------ */
668
+ // agent hook 的上报信使:用户在 claude/codex 的 hook 配置里指向本命令,事件发生时它把
669
+ // 事件名转发给本机 worker 的固定 gateway(POST /hook),供活动状态判定(执行中/等待交互)。
670
+ //
671
+ // 输入两种形态都收:claude 与 codex hooks 引擎走 stdin JSON;codex 旧式 notify 把 payload
672
+ // 作为最后一个 argv 传入。只转发事件名 + agent 会话 id + 本进程 pid/ppid——payload 里的
673
+ // prompt / 回答原文一律不出机(隐私边界)。
674
+ //
675
+ // 契约(worker 侧将来实现 /hook 时依赖):请求保持到收到响应才退出——worker 在处理期间
676
+ // 用上报的 pid 反查进程树归属哪个 session,本进程活着扫描才有效。
677
+ //
678
+ // 纪律:本命令绝不能干扰 agent 本体——任何失败(daemon 不在/端口不通/payload 畸形)都
679
+ // 静默退出 0(claude 把 Stop hook 的非零退出码解释为"阻止收尾");绝不写 stdout(claude
680
+ // 会把 hook 的 stdout 当决策 JSON 解析),调试信息走 stderr(COFLUX_HOOK_DEBUG=1 开启)。
681
+ const HOOK_STDIN_TIMEOUT_MS = 300; // stdin 没有数据时不能干等(notify 形态下 stdin 是继承的 TTY/空管道)
682
+ const HOOK_POST_TIMEOUT_MS = 2000;
683
+
684
+ const hookDebug = (...args) => { if (process.env.COFLUX_HOOK_DEBUG) console.error("[cofluxd hook]", ...args); };
685
+
686
+ async function readStdinJson() {
687
+ if (process.stdin.isTTY) return null;
688
+ const chunks = [];
689
+ const drained = (async () => { for await (const chunk of process.stdin) chunks.push(chunk); })().catch(() => {});
690
+ await Promise.race([drained, sleep(HOOK_STDIN_TIMEOUT_MS)]);
691
+ process.stdin.destroy(); // 超时后放掉 stdin,否则 for await 会吊着进程不退出
692
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
693
+ if (!raw) return null;
694
+ try { return JSON.parse(raw); } catch { return null; }
695
+ }
696
+
697
+ async function cmdHook() {
698
+ try {
699
+ const agent = positionals[1];
700
+ if (agent !== "claude" && agent !== "codex") {
701
+ hookDebug(`未知 agent: ${agent ?? "(缺参)"}(需 claude|codex)`);
702
+ return;
703
+ }
704
+ let payload = null;
705
+ if (positionals[2]) {
706
+ try { payload = JSON.parse(positionals[2]); } catch { /* 非 JSON 的多余参数,忽略 */ }
707
+ }
708
+ if (!payload) payload = await readStdinJson();
709
+ if (!payload || typeof payload !== "object") {
710
+ hookDebug("无有效 payload,忽略");
711
+ return;
712
+ }
713
+ // claude/codex hooks 引擎用 hook_event_name;codex notify 用 type
714
+ const event = payload.hook_event_name || payload.type;
715
+ if (typeof event !== "string" || !event) {
716
+ hookDebug("payload 缺事件名,忽略");
717
+ return;
718
+ }
719
+ const portResult = localGatewayPort();
720
+ if (!portResult.ok) {
721
+ hookDebug(portResult.error);
722
+ return;
723
+ }
724
+ const body = {
725
+ agent,
726
+ event,
727
+ pid: process.pid,
728
+ ppid: process.ppid,
729
+ // agent 自身的会话标识(claude: session_id / codex notify: thread-id),供 worker 去重与调试
730
+ agentSessionId: payload.session_id ?? payload["thread-id"] ?? undefined,
731
+ };
732
+ hookDebug("POST /hook", JSON.stringify(body));
733
+ const res = await fetch(`http://127.0.0.1:${portResult.port}/hook`, {
734
+ method: "POST",
735
+ headers: { "content-type": "application/json" },
736
+ body: JSON.stringify(body),
737
+ signal: AbortSignal.timeout(HOOK_POST_TIMEOUT_MS),
738
+ });
739
+ hookDebug(`响应 ${res.status}`);
740
+ } catch (error) {
741
+ hookDebug(error?.message || String(error));
742
+ } finally {
743
+ process.exit(0); // 无论成败都干净退出:不给 agent 留非零退出码,也不让残留句柄吊住进程
744
+ }
745
+ }
746
+
628
747
  const HELP = `cofluxd —— coflux daemon 管理
629
748
 
630
749
  cofluxd 首次=up(打印浏览器授权链接),已配置=status
631
750
  cofluxd up [flags] 幂等:首次装+起,已装则按当前 settings.json 重装服务并重启
632
751
  cofluxd status 服务器/登记(含"等待授权")/服务/连接状态
633
752
  cofluxd doctor 中心网络 + gateway bind/grant/loopback + daemon 状态分层自检
634
- cofluxd update 更新本地 supervisor 二进制并重启(worker server 自动热升级)
753
+ cofluxd update 下载新二进制(不重启;supervisor 有变化时提示用 restart 应用)
754
+ cofluxd restart 重启 daemon 应用新 supervisor(⚠ 结束本机所有活会话)
635
755
  cofluxd fda [仅 macOS] 引导授予完全磁盘访问权限(避免 PTY 因 TCC 弹窗卡住)
636
756
  cofluxd logs [-f] 看 daemon 日志
637
757
  cofluxd down 停止
638
758
  cofluxd uninstall [--purge] 卸载(--purge 连二进制/配置/凭证一并删)
759
+ cofluxd hook <claude|codex> [agent hook 信使] 读 stdin/argv 的事件 JSON,转发给本机 daemon
760
+ (在 claude/codex 的 hook 配置里指向本命令;失败静默,不干扰 agent)
639
761
 
640
762
  up flags: --server <ws://.../daemon> --name <名> --shell <路径>
641
763
  通用: --version <vX|latest>(不传时 up 沿用已有二进制,update 默认 latest) --bin-dir <dir>(用本地 cargo 产物) --no-start
@@ -667,7 +789,7 @@ let cmd = positionals[0];
667
789
  if (values.help || cmd === "help") { console.log(HELP); process.exit(0); }
668
790
  if (!cmd) cmd = fs.existsSync(SETTINGS) ? "status" : "up"; // 首次裸跑 → 引导
669
791
 
670
- const handlers = { up: cmdUp, update: cmdUpdate, down: cmdDown, status: cmdStatus, doctor: cmdDoctor, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall };
792
+ const handlers = { up: cmdUp, update: cmdUpdate, restart: cmdRestart, down: cmdDown, status: cmdStatus, doctor: cmdDoctor, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall, hook: cmdHook };
671
793
  const h = handlers[cmd];
672
794
  if (!h) die(`未知命令: ${cmd}${MIGRATED[cmd] ? `\n${MIGRATED[cmd]}` : ""}\n\n${HELP}`);
673
795
  await h(values);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cofluxd",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "coflux daemon 管理 CLI:装/起/停/升级 Rust daemon(supervisor + worker)",
5
5
  "type": "module",
6
6
  "bin": {