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.
- package/README.md +2 -1
- package/cofluxd.mjs +128 -6
- 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 #
|
|
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(
|
|
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
|
|
314
|
-
|
|
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
|
|
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);
|