cofluxd 2.9.0 → 2.11.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/coflux.mjs +4 -2
- package/cofluxd.mjs +104 -29
- package/package.json +1 -1
- package/release-trust.mjs +20 -6
- package/service-unit.mjs +49 -1
- package/skills/coflux/SKILL.md +36 -3
package/coflux.mjs
CHANGED
|
@@ -11,8 +11,10 @@ import { join } from "node:path";
|
|
|
11
11
|
const HOME = process.env.COFLUX_HOME || join(homedir(), ".coflux");
|
|
12
12
|
// Native integration owns explicit workspace selection and conversation state. `secret` is native
|
|
13
13
|
// only too: it speaks the worker's kernel-attested socket and masks child output, and one
|
|
14
|
-
// implementation of that is enough (plan 20260926-agent-secret-input).
|
|
15
|
-
|
|
14
|
+
// implementation of that is enough (plan 20260926-agent-secret-input). `annotations` is native only
|
|
15
|
+
// as well: the worker keeps browser annotations and the Rust CLI renders them (plan
|
|
16
|
+
// 20260929-browser-annotations).
|
|
17
|
+
if (process.argv[2] === "agent" || process.argv[2] === "secret" || process.argv[2] === "annotations" || (process.argv[2] === "workspace" && process.argv[3] === "enter")) {
|
|
16
18
|
const native = process.env.COFLUX_AGENT_BUNDLE
|
|
17
19
|
? join(process.env.COFLUX_AGENT_BUNDLE, "coflux")
|
|
18
20
|
: join(HOME, "bin", "coflux");
|
package/cofluxd.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// cofluxd —— coflux daemon 管理 CLI。
|
|
3
|
-
// daemon
|
|
4
|
-
// 本 CLI 只负责装/起/停/升级(用一下,不常驻)。systemd(Linux user) / launchd(macOS LaunchAgent)
|
|
3
|
+
// daemon 是三个 Rust 进程(ptyd 持 PTY、长生;supervisor 可随时替换;worker 频繁热升级,零 node 运行时);
|
|
4
|
+
// 本 CLI 只负责装/起/停/升级(用一下,不常驻)。systemd(Linux user) / launchd(macOS LaunchAgent),
|
|
5
|
+
// ptyd 与 supervisor 各自一份服务文件:重启 supervisor 不带走 ptyd,终端留在里面。
|
|
5
6
|
import { parseArgs } from "node:util";
|
|
6
7
|
import { homedir, hostname, platform, arch } from "node:os";
|
|
7
8
|
import { join, dirname } from "node:path";
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
parseReleaseManifestEntry,
|
|
22
23
|
verifyReleaseArtifact,
|
|
23
24
|
} from "./release-trust.mjs";
|
|
24
|
-
import { executorRuntime, plistXml, systemdUnit } from "./service-unit.mjs";
|
|
25
|
+
import { executorRuntime, plistXml, ptydPlistXml, ptydSystemdUnit, systemdUnit } from "./service-unit.mjs";
|
|
25
26
|
|
|
26
27
|
// 默认中心服务(公共 SaaS);自托管用 --server 覆盖。
|
|
27
28
|
const DEFAULT_SERVER = "wss://api.coflux.dev/daemon";
|
|
@@ -47,12 +48,19 @@ const LOCAL_GATEWAY_STORE = join(HOME, "local-gateway.json"); // gateway key/ori
|
|
|
47
48
|
const FDA_STATUS = join(HOME, "fda-status"); // supervisor 启动时探测落盘(仅 macOS,见 crates/supervisor/src/fda.rs)
|
|
48
49
|
const SUP_BIN = join(BIN_DIR, "coflux-supervisor");
|
|
49
50
|
const WRK_BIN = join(BIN_DIR, "coflux-worker");
|
|
51
|
+
// PTY 托管进程(plan 20260918-ptyd-terminal-custody):与 supervisor 平级的独立服务,supervisor 找不到它就拒绝启动。
|
|
52
|
+
const PTYD_BIN = join(BIN_DIR, "coflux-ptyd");
|
|
50
53
|
const CLI_RELEASE_FLOOR = join(HOME, "cofluxd.release-floor");
|
|
51
54
|
const WORKER_RELEASE_FLOOR = join(HOME, "worker.release-floor");
|
|
52
55
|
const IS_MAC = platform() === "darwin";
|
|
53
56
|
const IS_LINUX = platform() === "linux";
|
|
54
57
|
const PLIST = join(homedir(), "Library", "LaunchAgents", "com.coflux.daemon.plist");
|
|
55
58
|
const UNIT = join(homedir(), ".config", "systemd", "user", "coflux-daemon.service");
|
|
59
|
+
// ptyd 是独立的服务(plan 20260918-ptyd-terminal-custody):launchd 一个 plist 只能跑一个程序,systemd 默认
|
|
60
|
+
// KillMode=control-group 会在 `systemctl restart` 时把整个 cgroup 一起杀——ptyd 必须在 supervisor 的进程组 /
|
|
61
|
+
// cgroup 之外,重启 supervisor 才不会带走它。停止 daemon(down / uninstall)仍然一并停 ptyd。
|
|
62
|
+
const PTYD_PLIST = join(homedir(), "Library", "LaunchAgents", "com.coflux.ptyd.plist");
|
|
63
|
+
const PTYD_UNIT = join(homedir(), ".config", "systemd", "user", "coflux-ptyd.service");
|
|
56
64
|
const DEFAULT_LOCAL_GATEWAY_PORT = 8788; // 与 packages/crates protocol 的 LOCAL_GATEWAY_PORT 保持一致
|
|
57
65
|
|
|
58
66
|
const die = (m) => { console.error("✗ " + m); process.exit(1); };
|
|
@@ -270,7 +278,7 @@ async function resolveLatestTag() {
|
|
|
270
278
|
async function ensureBinaries({ version, binDir, skipIfPresent }) {
|
|
271
279
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
272
280
|
if (binDir) {
|
|
273
|
-
const localArtifacts = ["coflux-supervisor", "coflux-worker", ...(fs.existsSync(join(binDir, "coflux")) ? ["coflux"] : []), ...(fs.existsSync(join(binDir, "coflux-transport")) ? ["coflux-transport"] : [])].map((name) => ({
|
|
281
|
+
const localArtifacts = ["coflux-supervisor", "coflux-worker", "coflux-ptyd", ...(fs.existsSync(join(binDir, "coflux")) ? ["coflux"] : []), ...(fs.existsSync(join(binDir, "coflux-transport")) ? ["coflux-transport"] : [])].map((name) => ({
|
|
274
282
|
name,
|
|
275
283
|
path: join(binDir, name),
|
|
276
284
|
}));
|
|
@@ -308,7 +316,7 @@ async function ensureBinaries({ version, binDir, skipIfPresent }) {
|
|
|
308
316
|
console.log(`✓ 用本地二进制(${binDir})`);
|
|
309
317
|
return;
|
|
310
318
|
}
|
|
311
|
-
if (skipIfPresent && !version && fs.existsSync(SUP_BIN) && fs.existsSync(WRK_BIN)) {
|
|
319
|
+
if (skipIfPresent && !version && fs.existsSync(SUP_BIN) && fs.existsSync(WRK_BIN) && fs.existsSync(PTYD_BIN)) {
|
|
312
320
|
console.log(`✓ 二进制已存在(${BIN_DIR}),跳过下载(用 cofluxd update 升级)`);
|
|
313
321
|
return;
|
|
314
322
|
}
|
|
@@ -357,7 +365,7 @@ async function ensureBinaries({ version, binDir, skipIfPresent }) {
|
|
|
357
365
|
}
|
|
358
366
|
const publicKey = loadReleasePublicKey();
|
|
359
367
|
const staged = [];
|
|
360
|
-
for (const component of ["supervisor", "worker", ...(manifest.cli ? ["cli"] : []), ...(manifest.transport ? ["transport"] : [])]) {
|
|
368
|
+
for (const component of ["supervisor", "worker", ...(manifest.cli ? ["cli"] : []), ...(manifest.transport ? ["transport"] : []), ...(manifest.ptyd ? ["ptyd"] : [])]) {
|
|
361
369
|
const entry = parseReleaseManifestEntry(manifest, component, releaseVersion, target);
|
|
362
370
|
const artifactName = `coflux-${component}-${target}`;
|
|
363
371
|
process.stdout.write(`下载并验签 ${artifactName} … `);
|
|
@@ -372,7 +380,7 @@ async function ensureBinaries({ version, binDir, skipIfPresent }) {
|
|
|
372
380
|
fs.chmodSync(source, 0o755);
|
|
373
381
|
staged.push({
|
|
374
382
|
source,
|
|
375
|
-
destination: component === "cli" ? join(BIN_DIR, "coflux") : component === "supervisor" ? SUP_BIN : component === "transport" ? join(BIN_DIR, "coflux-transport") : WRK_BIN,
|
|
383
|
+
destination: component === "cli" ? join(BIN_DIR, "coflux") : component === "supervisor" ? SUP_BIN : component === "transport" ? join(BIN_DIR, "coflux-transport") : component === "ptyd" ? PTYD_BIN : WRK_BIN,
|
|
376
384
|
});
|
|
377
385
|
console.log("✓");
|
|
378
386
|
}
|
|
@@ -421,26 +429,67 @@ function executorUnitRuntime() {
|
|
|
421
429
|
return runtime;
|
|
422
430
|
}
|
|
423
431
|
|
|
432
|
+
/** 四份服务文件的路径与文本(`cofluxd service-files` 原样打印,黑盒据此断言 ptyd 独立受管)。 */
|
|
433
|
+
function serviceFiles(executor = executorRuntime()) {
|
|
434
|
+
return {
|
|
435
|
+
launchd: {
|
|
436
|
+
supervisor: { path: PLIST, text: plistXml({ supervisorBin: SUP_BIN, home: HOME, logFile: LOG_FILE, executor }) },
|
|
437
|
+
ptyd: { path: PTYD_PLIST, text: ptydPlistXml({ ptydBin: PTYD_BIN, home: HOME, logFile: LOG_FILE }) },
|
|
438
|
+
},
|
|
439
|
+
systemd: {
|
|
440
|
+
supervisor: { path: UNIT, text: systemdUnit({ supervisorBin: SUP_BIN, home: HOME, executor }) },
|
|
441
|
+
ptyd: { path: PTYD_UNIT, text: ptydSystemdUnit({ ptydBin: PTYD_BIN, home: HOME }) },
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
424
445
|
function installService(start) {
|
|
446
|
+
const files = serviceFiles(executorUnitRuntime());
|
|
425
447
|
if (IS_MAC) {
|
|
426
448
|
fs.mkdirSync(dirname(PLIST), { recursive: true });
|
|
427
|
-
fs.writeFileSync(
|
|
428
|
-
|
|
429
|
-
|
|
449
|
+
fs.writeFileSync(PTYD_PLIST, files.launchd.ptyd.text);
|
|
450
|
+
fs.writeFileSync(PLIST, files.launchd.supervisor.text);
|
|
451
|
+
if (start) {
|
|
452
|
+
// ptyd 先起:supervisor 连不上它会退出,KeepAlive 再拉一次也只是浪费。
|
|
453
|
+
run("launchctl", ["unload", PLIST]);
|
|
454
|
+
run("launchctl", ["unload", PTYD_PLIST]);
|
|
455
|
+
run("launchctl", ["load", PTYD_PLIST]);
|
|
456
|
+
run("launchctl", ["load", PLIST]);
|
|
457
|
+
}
|
|
458
|
+
console.log(`✓ launchd: ${PTYD_PLIST}\n✓ launchd: ${PLIST}`);
|
|
430
459
|
} else if (IS_LINUX) {
|
|
431
460
|
fs.mkdirSync(dirname(UNIT), { recursive: true });
|
|
432
|
-
fs.writeFileSync(
|
|
433
|
-
|
|
434
|
-
|
|
461
|
+
fs.writeFileSync(PTYD_UNIT, files.systemd.ptyd.text);
|
|
462
|
+
fs.writeFileSync(UNIT, files.systemd.supervisor.text);
|
|
463
|
+
if (start) {
|
|
464
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
465
|
+
run("systemctl", ["--user", "enable", "--now", "coflux-ptyd.service"]);
|
|
466
|
+
run("systemctl", ["--user", "enable", "--now", "coflux-daemon.service"]);
|
|
467
|
+
}
|
|
468
|
+
console.log(`✓ systemd: ${PTYD_UNIT}\n✓ systemd: ${UNIT}`);
|
|
435
469
|
} else die("仅支持 macOS / Linux");
|
|
436
470
|
}
|
|
471
|
+
/** 只重启 supervisor:终端留在 ptyd 里。ptyd 若还没起(首次升到带 ptyd 的版本)先把它拉起来,已在跑则无事。 */
|
|
437
472
|
function restartService() {
|
|
438
|
-
if (IS_MAC) {
|
|
439
|
-
|
|
473
|
+
if (IS_MAC) {
|
|
474
|
+
run("launchctl", ["load", PTYD_PLIST]);
|
|
475
|
+
run("launchctl", ["unload", PLIST]);
|
|
476
|
+
run("launchctl", ["load", PLIST]);
|
|
477
|
+
} else if (IS_LINUX) {
|
|
478
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
479
|
+
run("systemctl", ["--user", "enable", "--now", "coflux-ptyd.service"]);
|
|
480
|
+
run("systemctl", ["--user", "restart", "coflux-daemon.service"]);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/** 重启两者(ptyd 自身更新):会结束本机全部终端。 */
|
|
484
|
+
function restartAllServices() {
|
|
485
|
+
stopService();
|
|
486
|
+
if (IS_MAC) { run("launchctl", ["load", PTYD_PLIST]); run("launchctl", ["load", PLIST]); }
|
|
487
|
+
else if (IS_LINUX) { run("systemctl", ["--user", "start", "coflux-ptyd.service"]); run("systemctl", ["--user", "start", "coflux-daemon.service"]); }
|
|
440
488
|
}
|
|
489
|
+
/** 停止本机 daemon = supervisor + ptyd 一起停(终端结束)。 */
|
|
441
490
|
function stopService() {
|
|
442
|
-
if (IS_MAC) run("launchctl", ["unload", PLIST]);
|
|
443
|
-
else if (IS_LINUX) run("systemctl", ["--user", "stop", "coflux-daemon.service"]);
|
|
491
|
+
if (IS_MAC) { run("launchctl", ["unload", PLIST]); run("launchctl", ["unload", PTYD_PLIST]); }
|
|
492
|
+
else if (IS_LINUX) { run("systemctl", ["--user", "stop", "coflux-daemon.service"]); run("systemctl", ["--user", "stop", "coflux-ptyd.service"]); }
|
|
444
493
|
}
|
|
445
494
|
|
|
446
495
|
async function applyAndStart({ serverUrl, deviceName, shell, version, binDir, noStart }) {
|
|
@@ -582,24 +631,46 @@ async function cmdUpdate(v) {
|
|
|
582
631
|
if (!fs.existsSync(SETTINGS)) die("尚未安装,先 cofluxd up");
|
|
583
632
|
const version = v.version || "latest";
|
|
584
633
|
const before = fileSha256(SUP_BIN);
|
|
634
|
+
const ptydBefore = fileSha256(PTYD_BIN);
|
|
585
635
|
await ensureBinaries({ version, binDir: v["bin-dir"] });
|
|
586
|
-
|
|
587
|
-
|
|
636
|
+
const supervisorChanged = before !== fileSha256(SUP_BIN);
|
|
637
|
+
const ptydChanged = ptydBefore !== fileSha256(PTYD_BIN);
|
|
638
|
+
if (!supervisorChanged && !ptydChanged) {
|
|
639
|
+
console.log("✓ 已更新:supervisor / ptyd 无变化——不需要重启,活会话不受影响");
|
|
588
640
|
console.log(" worker 由 server 自动热升级;本次刷新的只是兜底 worker 二进制。");
|
|
589
|
-
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (supervisorChanged) {
|
|
590
644
|
console.log(`✓ 新 supervisor 已就位(${v["bin-dir"] ? "本地产物" : version}),但尚未生效`);
|
|
591
|
-
console.log("
|
|
645
|
+
console.log(" 运行 `cofluxd restart` 应用——终端留在 ptyd 里,屏幕与回滚原样保留,只有几秒停顿。");
|
|
646
|
+
}
|
|
647
|
+
if (ptydChanged) {
|
|
648
|
+
console.log(`✓ 新 ptyd(终端托管进程)已就位${ptydBefore ? "" : "(首次安装)"},但尚未生效`);
|
|
649
|
+
console.log(" ⚠ 运行 `cofluxd restart --ptyd` 应用——ptyd 自身更新无法保留终端,会结束本机所有活会话。");
|
|
592
650
|
}
|
|
593
651
|
}
|
|
594
652
|
|
|
595
|
-
//
|
|
596
|
-
|
|
653
|
+
// 应用已就位的新 supervisor(或单纯重启 supervisor):终端留在 ptyd 里不动。
|
|
654
|
+
// `--ptyd`:连 ptyd 一起重启(ptyd 自身更新时才需要)——这才是会结束本机会话的那条命令。
|
|
655
|
+
function cmdRestart(v) {
|
|
597
656
|
if (!fs.existsSync(SETTINGS)) die("尚未安装,先 cofluxd up");
|
|
598
|
-
|
|
599
|
-
|
|
657
|
+
// 服务文件与模板同步(首次升到带 ptyd 的版本时 ptyd 的 plist / unit 还不存在);只写文件,不在这里启动。
|
|
658
|
+
installService(false);
|
|
659
|
+
if (v.ptyd) {
|
|
660
|
+
console.log("⚠ 正在重启 ptyd 与 supervisor:本机所有活会话将结束…");
|
|
661
|
+
restartAllServices();
|
|
662
|
+
} else {
|
|
663
|
+
console.log("正在重启 supervisor:本机终端留在 ptyd 里,屏幕与回滚原样保留…");
|
|
664
|
+
restartService();
|
|
665
|
+
}
|
|
600
666
|
console.log("✓ 已重启(cofluxd status 查看状态)");
|
|
601
667
|
}
|
|
602
668
|
|
|
669
|
+
/** 隐藏命令:把四份服务文件(launchd × 2、systemd × 2)以 JSON 打到 stdout,供黑盒断言与运维核对。 */
|
|
670
|
+
function cmdServiceFiles() {
|
|
671
|
+
console.log(JSON.stringify(serviceFiles(), null, 2));
|
|
672
|
+
}
|
|
673
|
+
|
|
603
674
|
// launchd(macOS) / systemd --user(Linux) 服务活跃态查询,status 与 doctor 共用。
|
|
604
675
|
// macOS 必须以 launchctl list 输出里的 PID 为准:label 登记 ≠ 进程存活(supervisor 被
|
|
605
676
|
// OS_REASON_CODESIGNING 杀掉时 label 仍在,旧实现会报假"运行中",2026-07-25 实测踩过)。
|
|
@@ -916,7 +987,9 @@ async function cmdFda() {
|
|
|
916
987
|
|
|
917
988
|
function cmdUninstall(v) {
|
|
918
989
|
stopService();
|
|
919
|
-
|
|
990
|
+
for (const file of IS_MAC ? [PLIST, PTYD_PLIST] : [UNIT, PTYD_UNIT]) {
|
|
991
|
+
try { fs.rmSync(file); } catch { /* */ }
|
|
992
|
+
}
|
|
920
993
|
if (IS_LINUX) run("systemctl", ["--user", "daemon-reload"]);
|
|
921
994
|
if (v.purge) { try { fs.rmSync(HOME, { recursive: true, force: true }); } catch { /* */ } console.log("✓ 已卸载并清除 " + HOME); }
|
|
922
995
|
else console.log(`✓ 已卸载服务(保留二进制/配置/凭证于 ${HOME};--purge 可全清)`);
|
|
@@ -929,8 +1002,9 @@ const HELP = `cofluxd —— coflux daemon 管理
|
|
|
929
1002
|
cofluxd up --key <密钥> 用 Coflux 添加设备里生成的一次性密钥直接接入账号(不打印授权链接;已接入则不做改动)
|
|
930
1003
|
cofluxd status 服务器/登记(含"等待授权")/服务/连接状态
|
|
931
1004
|
cofluxd doctor 中心网络 + gateway bind/grant/loopback + daemon 状态分层自检
|
|
932
|
-
cofluxd update 下载新二进制(不重启;supervisor 有变化时提示用 restart 应用)
|
|
933
|
-
cofluxd restart 重启
|
|
1005
|
+
cofluxd update 下载新二进制(不重启;supervisor / ptyd 有变化时提示用 restart 应用)
|
|
1006
|
+
cofluxd restart 重启 supervisor 应用新版本(终端留在 ptyd 里,不结束会话)
|
|
1007
|
+
cofluxd restart --ptyd 连 ptyd 一起重启(⚠ 结束本机所有活会话;仅 ptyd 自身更新时需要)
|
|
934
1008
|
cofluxd fda [仅 macOS] 引导授予完全磁盘访问权限(避免 PTY 因 TCC 弹窗卡住)
|
|
935
1009
|
cofluxd logs [-f] 看 daemon 日志
|
|
936
1010
|
cofluxd down 停止
|
|
@@ -946,12 +1020,13 @@ const { values, positionals } = parseArgs({
|
|
|
946
1020
|
server: { type: "string" }, name: { type: "string" }, shell: { type: "string" }, key: { type: "string" },
|
|
947
1021
|
version: { type: "string" }, "bin-dir": { type: "string" },
|
|
948
1022
|
"no-start": { type: "boolean", default: false }, purge: { type: "boolean", default: false },
|
|
1023
|
+
ptyd: { type: "boolean", default: false },
|
|
949
1024
|
follow: { type: "boolean", short: "f", default: false }, help: { type: "boolean", short: "h", default: false },
|
|
950
1025
|
},
|
|
951
1026
|
});
|
|
952
1027
|
const cmd = positionals[0] || (fs.existsSync(SETTINGS) ? "status" : "up");
|
|
953
1028
|
if (values.help || cmd === "help") { console.log(HELP); process.exit(0); }
|
|
954
|
-
const handlers = { up: cmdUp, update: cmdUpdate, restart: cmdRestart, down: cmdDown, status: cmdStatus, doctor: cmdDoctor, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall };
|
|
1029
|
+
const handlers = { up: cmdUp, update: cmdUpdate, restart: cmdRestart, down: cmdDown, status: cmdStatus, doctor: cmdDoctor, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall, "service-files": cmdServiceFiles };
|
|
955
1030
|
const handler = handlers[cmd];
|
|
956
1031
|
if (!handler) die(`未知命令: ${cmd}\n登录与终端操作请使用 coflux。\n\n${HELP}`);
|
|
957
1032
|
await handler(values);
|
package/package.json
CHANGED
package/release-trust.mjs
CHANGED
|
@@ -17,6 +17,22 @@ export const CLI_RELEASE_STATEMENT_DOMAIN = Buffer.from("coflux-cli-release-v1\0
|
|
|
17
17
|
export const TRANSPORT_RELEASE_STATEMENT_DOMAIN = Buffer.from("coflux-transport-release-v1\0", "utf8");
|
|
18
18
|
export function transportReleaseStatement(metadata) { return artifactReleaseStatement(TRANSPORT_RELEASE_STATEMENT_DOMAIN, metadata); }
|
|
19
19
|
|
|
20
|
+
/** PTY 托管进程(plan 20260918-ptyd-terminal-custody):独立 domain,合法的其它组件签名不能移植过来。 */
|
|
21
|
+
export const PTYD_RELEASE_STATEMENT_DOMAIN = Buffer.from("coflux-ptyd-release-v1\0", "utf8");
|
|
22
|
+
export function ptydReleaseStatement(metadata) { return artifactReleaseStatement(PTYD_RELEASE_STATEMENT_DOMAIN, metadata); }
|
|
23
|
+
|
|
24
|
+
const RELEASE_COMPONENTS = ["worker", "supervisor", "cli", "transport", "ptyd"];
|
|
25
|
+
|
|
26
|
+
function releaseStatementFor(component, metadata) {
|
|
27
|
+
switch (component) {
|
|
28
|
+
case "worker": return workerReleaseStatement(metadata);
|
|
29
|
+
case "cli": return cliReleaseStatement(metadata);
|
|
30
|
+
case "transport": return transportReleaseStatement(metadata);
|
|
31
|
+
case "ptyd": return ptydReleaseStatement(metadata);
|
|
32
|
+
default: return supervisorReleaseStatement(metadata);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
const STRICT_RELEASE_VERSION = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
21
37
|
const SHA256_HEX = /^[0-9a-f]{64}$/i;
|
|
22
38
|
const ED25519_SIGNATURE_HEX = /^[0-9a-f]{128}$/i;
|
|
@@ -146,7 +162,7 @@ function isRecord(value) {
|
|
|
146
162
|
*/
|
|
147
163
|
export function parseReleaseManifestEntry(manifest, component, version, target) {
|
|
148
164
|
assertReleaseVersion(version);
|
|
149
|
-
if (!
|
|
165
|
+
if (!RELEASE_COMPONENTS.includes(component)) {
|
|
150
166
|
throw new Error(`未知 release component: ${JSON.stringify(component)}`);
|
|
151
167
|
}
|
|
152
168
|
if (!isRecord(manifest) || manifest.schemaVersion !== 2 || manifest.version !== version) {
|
|
@@ -200,9 +216,7 @@ export function verifyReleaseArtifact({ component, version, entry, data, publicK
|
|
|
200
216
|
throw new Error("worker 产物 legacy Ed25519 签名无效");
|
|
201
217
|
}
|
|
202
218
|
const metadata = { version, target: entry.target, sha256, size: data.byteLength };
|
|
203
|
-
const statement = component
|
|
204
|
-
? workerReleaseStatement(metadata)
|
|
205
|
-
: component === "cli" ? cliReleaseStatement(metadata) : component === "transport" ? transportReleaseStatement(metadata) : supervisorReleaseStatement(metadata);
|
|
219
|
+
const statement = releaseStatementFor(component, metadata);
|
|
206
220
|
if (!crypto.verify(null, statement, publicKey, Buffer.from(entry.releaseSignature, "hex"))) {
|
|
207
221
|
throw new Error(`${component} 产物 release Ed25519 签名无效`);
|
|
208
222
|
}
|
|
@@ -215,11 +229,11 @@ export function verifyReleaseArtifact({ component, version, entry, data, publicK
|
|
|
215
229
|
export function installStagedPair(staged) {
|
|
216
230
|
if (
|
|
217
231
|
!Array.isArray(staged) ||
|
|
218
|
-
|
|
232
|
+
staged.length < 2 || staged.length > 6 ||
|
|
219
233
|
staged.some(({ source, destination }) =>
|
|
220
234
|
typeof source !== "string" || !source || typeof destination !== "string" || !destination)
|
|
221
235
|
) {
|
|
222
|
-
throw new Error("daemon installation requires two to
|
|
236
|
+
throw new Error("daemon installation requires two to six valid staged artifacts");
|
|
223
237
|
}
|
|
224
238
|
const installed = [];
|
|
225
239
|
const backups = [];
|
package/service-unit.mjs
CHANGED
|
@@ -80,6 +80,53 @@ ${entries}
|
|
|
80
80
|
`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* ptyd (plan 20260918-ptyd-terminal-custody) runs under its own label: the supervisor's SIGTERM
|
|
85
|
+
* leaves sessions in ptyd, so `launchctl unload/load com.coflux.daemon` touches only the supervisor,
|
|
86
|
+
* and KeepAlive brings ptyd back on its own if it crashes.
|
|
87
|
+
*/
|
|
88
|
+
export function ptydPlistXml({ ptydBin, home, logFile }) {
|
|
89
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
90
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
91
|
+
<plist version="1.0">
|
|
92
|
+
<dict>
|
|
93
|
+
<key>Label</key><string>com.coflux.ptyd</string>
|
|
94
|
+
<key>ProgramArguments</key>
|
|
95
|
+
<array><string>${xml(ptydBin)}</string></array>
|
|
96
|
+
<key>EnvironmentVariables</key>
|
|
97
|
+
<dict>
|
|
98
|
+
<key>COFLUX_HOME</key><string>${xml(home)}</string>
|
|
99
|
+
</dict>
|
|
100
|
+
<key>RunAtLoad</key><true/>
|
|
101
|
+
<key>KeepAlive</key><true/>
|
|
102
|
+
<key>StandardOutPath</key><string>${xml(logFile)}</string>
|
|
103
|
+
<key>StandardErrorPath</key><string>${xml(logFile)}</string>
|
|
104
|
+
</dict>
|
|
105
|
+
</plist>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* systemd: ptyd has its own unit and the supervisor's unit Requires/After it. `systemctl restart
|
|
111
|
+
* coflux-daemon` restarts only the supervisor's cgroup; ptyd's is not part of it, so terminals are
|
|
112
|
+
* untouched. Stopping ptyd (down) also stops the supervisor (Requires).
|
|
113
|
+
*/
|
|
114
|
+
export function ptydSystemdUnit({ ptydBin, home }) {
|
|
115
|
+
const environment = oneLine(home) ? `Environment=COFLUX_HOME=${home}\n` : "";
|
|
116
|
+
return `[Unit]
|
|
117
|
+
Description=coflux ptyd (terminal custody)
|
|
118
|
+
|
|
119
|
+
[Service]
|
|
120
|
+
${environment}ExecStart=${ptydBin}
|
|
121
|
+
Restart=always
|
|
122
|
+
RestartSec=2
|
|
123
|
+
KillMode=process
|
|
124
|
+
|
|
125
|
+
[Install]
|
|
126
|
+
WantedBy=default.target
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
83
130
|
export function systemdUnit({ supervisorBin, home, executor }) {
|
|
84
131
|
const variables = [["COFLUX_HOME", home]];
|
|
85
132
|
if (executor) {
|
|
@@ -91,8 +138,9 @@ export function systemdUnit({ supervisorBin, home, executor }) {
|
|
|
91
138
|
.join("\n");
|
|
92
139
|
return `[Unit]
|
|
93
140
|
Description=coflux daemon (supervisor)
|
|
94
|
-
After=network-online.target
|
|
141
|
+
After=network-online.target coflux-ptyd.service
|
|
95
142
|
Wants=network-online.target
|
|
143
|
+
Requires=coflux-ptyd.service
|
|
96
144
|
|
|
97
145
|
[Service]
|
|
98
146
|
${environment}
|
package/skills/coflux/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: coflux
|
|
3
|
-
description: Use coflux to enter workspaces, open terminals the user can see and take over, run commands in them, wait for those commands, read their scrollback, type into them, run one-shot commands on another device in the account and get their output, report progress, notify the user, obtain preview URLs, get a secret (API key, password) from the user without the value entering your context, and hand a bounded mechanical sub-task to the built-in executor instead of spending your own context on it. Prefer zero-credential local commands in the current workspace; use the account CLI across workspaces and devices. Coordinates arrive through coflux-session or COFLUX_* variables.
|
|
3
|
+
description: Use coflux to enter workspaces, open terminals the user can see and take over, run commands in them, wait for those commands, read their scrollback, type into them, run one-shot commands on another device in the account and get their output, report progress, notify the user, obtain preview URLs, get a secret (API key, password) from the user without the value entering your context, pick up the elements the user annotated in Coflux's built-in browser and mark them done, and hand a bounded mechanical sub-task to the built-in executor instead of spending your own context on it. Prefer zero-credential local commands in the current workspace; use the account CLI across workspaces and devices. Coordinates arrive through coflux-session or COFLUX_* variables.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Working inside coflux
|
|
@@ -14,10 +14,10 @@ and a way to operate the other workspaces and devices under the account when you
|
|
|
14
14
|
|
|
15
15
|
| Track | Credentials | Reach | Use for |
|
|
16
16
|
|---|---|---|---|
|
|
17
|
-
| Local commands `coflux terminal/progress/notify/ports/executor/secret` | none (the daemon identifies you by process tree) | **the workspace your cwd is in** | open, run, wait, read, send, close, report progress, call the user, preview URLs, get a secret from the user, hand a bounded sub-task to the built-in executor: the default; some actions require a server connection |
|
|
17
|
+
| Local commands `coflux terminal/progress/notify/ports/executor/secret/annotations` | none (the daemon identifies you by process tree) | **the workspace your cwd is in** | open, run, wait, read, send, close, report progress, call the user, preview URLs, get a secret from the user, implement the user's browser annotations, hand a bounded sub-task to the built-in executor: the default; some actions require a server connection |
|
|
18
18
|
| Account CLI | app login or `coflux login` | all devices and workspaces in the account | child workspaces and remote terminals; JSON output |
|
|
19
19
|
|
|
20
|
-
Of the local commands, `run`/`wait`/`read`/`send`/`close`/`progress`/`executor` complete
|
|
20
|
+
Of the local commands, `run`/`wait`/`read`/`send`/`close`/`progress`/`executor`/`annotations` complete
|
|
21
21
|
entirely inside the local daemon and never touch the center; `new`/`list`/`ports`/`notify` are relayed to the
|
|
22
22
|
center by the daemon on your behalf (terminals and notification history are persisted centrally; preview URLs are
|
|
23
23
|
minted by the center). You only ever talk to the local daemon.
|
|
@@ -510,6 +510,39 @@ executor cannot use `coflux secret` (it is not a terminal process). And it preve
|
|
|
510
510
|
a determined agent: once a value is in an environment variable or a file you can read, printing it
|
|
511
511
|
on purpose would leak it — never do that.
|
|
512
512
|
|
|
513
|
+
### Implement the user's browser annotations
|
|
514
|
+
|
|
515
|
+
```sh
|
|
516
|
+
coflux annotations list
|
|
517
|
+
coflux annotations resolve 3 --note "Primary button now uses the brand token; spacing from the 8px scale"
|
|
518
|
+
coflux annotations watch
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
The user can point at elements of a page in Coflux's built-in browser tab ("this element — change it
|
|
522
|
+
like so") and hand them to you; the desktop then types an instruction into your terminal, or they
|
|
523
|
+
simply ask you to handle the annotations. They belong to the workspace your cwd is in and live on
|
|
524
|
+
this machine, so they are there whether or not a desktop is open.
|
|
525
|
+
|
|
526
|
+
- `list` prints the pending ones as markdown: the user's comment (and, for a reopened one, your
|
|
527
|
+
earlier note and what the user answered), the page, the component chain and source location when
|
|
528
|
+
the page exposed them, the element's selector, DOM path and key computed styles, and the paths of
|
|
529
|
+
its images. It always names the workspace it resolved: an empty list in the wrong workspace means
|
|
530
|
+
you moved (`coflux workspace`). Add `--json` for structured output.
|
|
531
|
+
- Find the code from the most specific lead: a source location first, then the component names,
|
|
532
|
+
then text and selectors (`rg` for them). Treat the computed styles as the current state, not as
|
|
533
|
+
the target: map colors, sizes and spacing onto the project's design system (its tokens, theme and
|
|
534
|
+
components) instead of hard-coding raw values.
|
|
535
|
+
- The images are local files: read them. A *screenshot of the current state* shows the element as
|
|
536
|
+
the user saw it; a *reference image* is what the user wants or pointed at.
|
|
537
|
+
- After implementing each annotation, run `resolve <id or number> --note "<what you changed>"`.
|
|
538
|
+
The note is what the user reads to review the change; the pin on their page turns into a check.
|
|
539
|
+
They confirm it (it disappears) or reopen it with a comment, and it comes back in `list`. Resolve
|
|
540
|
+
only what you actually changed; if you cannot or should not do one, resolve it with a note that
|
|
541
|
+
says why rather than leaving it pending silently.
|
|
542
|
+
- `watch` blocks until the workspace has pending annotations and then prints them like `list`
|
|
543
|
+
(default budget 30 minutes, `--timeout <seconds>`). Use it only when the user asked you to keep
|
|
544
|
+
handling annotations hands-free: loop list → implement → resolve → `watch`.
|
|
545
|
+
|
|
513
546
|
### Errors from local commands
|
|
514
547
|
|
|
515
548
|
Errors are one readable sentence; do what they say: "not inside a coflux terminal" = you are not
|