cofluxd 2.14.0 → 2.16.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 +27 -2
- package/account-client.mjs +69 -45
- package/coflux.mjs +226 -150
- package/cofluxd.mjs +885 -427
- package/output.mjs +93 -0
- package/package.json +3 -2
- package/release-trust.mjs +44 -36
- package/service-unit.mjs +52 -13
- package/skills/coflux/SKILL.md +6 -4
package/output.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// The one place cofluxd and coflux decide how a message looks: symbols, colour, the error shape.
|
|
2
|
+
//
|
|
3
|
+
// - `✓` success and plain lines go to stdout; `!` warnings and `✗ Error:` go to stderr.
|
|
4
|
+
// - Every error is one line saying what happened plus one indented line saying what to do next.
|
|
5
|
+
// - Colour only when the stream is a TTY and NO_COLOR is unset (https://no-color.org) and TERM is
|
|
6
|
+
// not "dumb". Agents call coflux without a TTY and always get plain text.
|
|
7
|
+
//
|
|
8
|
+
// Hand-rolled SGR on purpose: `util.styleText` does not exist before Node 20.12 and colours
|
|
9
|
+
// unconditionally before 22.13, and package.json promises Node 20.
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
|
|
12
|
+
const SGR = { green: [32, 39], yellow: [33, 39], red: [31, 39], bold: [1, 22], dim: [2, 22] };
|
|
13
|
+
|
|
14
|
+
/** Whether `stream` may carry colour. */
|
|
15
|
+
export function colorEnabled(stream) {
|
|
16
|
+
if (!stream?.isTTY) return false;
|
|
17
|
+
const noColor = process.env.NO_COLOR;
|
|
18
|
+
if (noColor !== undefined && noColor !== "") return false;
|
|
19
|
+
return process.env.TERM !== "dumb";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function paint(stream, style, text) {
|
|
23
|
+
if (!colorEnabled(stream)) return text;
|
|
24
|
+
const [open, close] = SGR[style];
|
|
25
|
+
return `\u001b[${open}m${text}\u001b[${close}m`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Bold for emphasis on stdout (a URL, a command to run). */
|
|
29
|
+
export const bold = (text, stream = process.stdout) => paint(stream, "bold", text);
|
|
30
|
+
export const dim = (text, stream = process.stdout) => paint(stream, "dim", text);
|
|
31
|
+
|
|
32
|
+
/** A plain line on stdout. */
|
|
33
|
+
export function info(message = "") {
|
|
34
|
+
process.stdout.write(`${message}\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `✓ message` on stdout. */
|
|
38
|
+
export function success(message) {
|
|
39
|
+
process.stdout.write(`${paint(process.stdout, "green", "✓")} ${message}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `! message` on stderr, with an optional indented next step. */
|
|
43
|
+
export function warn(message, next) {
|
|
44
|
+
let text = `${paint(process.stderr, "yellow", "!")} ${message}\n`;
|
|
45
|
+
if (next) text += next.split("\n").map((line) => ` ${line}\n`).join("");
|
|
46
|
+
process.stderr.write(text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `✗ Error: what` plus an indented next step, on stderr. */
|
|
50
|
+
export function error(what, next) {
|
|
51
|
+
let text = `${paint(process.stderr, "red", "✗ Error:")} ${what}\n`;
|
|
52
|
+
if (next) text += next.split("\n").map((line) => ` ${line}\n`).join("");
|
|
53
|
+
process.stderr.write(text);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Print an error and exit. The write completes before the exit, even into a pipe. */
|
|
57
|
+
export function fail(what, next, code = 1) {
|
|
58
|
+
error(what, next);
|
|
59
|
+
// A message this short is written in full before process.exit: libuv tries the write
|
|
60
|
+
// synchronously first and only queues what the pipe cannot take at once.
|
|
61
|
+
process.exit(code);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `Doing X... ` now, then `done` (or a failure word) when the step ends. */
|
|
65
|
+
export function step(label) {
|
|
66
|
+
process.stdout.write(`${label}... `);
|
|
67
|
+
return {
|
|
68
|
+
done(word = "done") { process.stdout.write(`${word}\n`); },
|
|
69
|
+
failed(word = "failed") { process.stdout.write(`${word}\n`); },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Aligned key/value rows on stdout; rows whose value is undefined or null are skipped. */
|
|
74
|
+
export function table(rows) {
|
|
75
|
+
const shown = rows.filter(([, value]) => value !== undefined && value !== null);
|
|
76
|
+
const width = Math.max(0, ...shown.map(([key]) => key.length));
|
|
77
|
+
for (const [key, value] of shown) {
|
|
78
|
+
const lines = String(value).split("\n");
|
|
79
|
+
process.stdout.write(`${key.padEnd(width)} ${lines[0]}\n`);
|
|
80
|
+
for (const line of lines.slice(1)) process.stdout.write(`${" ".repeat(width)} ${line}\n`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A short human duration: `45s`, `12m`, `3h 12m`, `2d 4h`. */
|
|
85
|
+
export function duration(ms) {
|
|
86
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
87
|
+
if (seconds < 60) return `${seconds}s`;
|
|
88
|
+
const minutes = Math.floor(seconds / 60);
|
|
89
|
+
if (minutes < 60) return `${minutes}m`;
|
|
90
|
+
const hours = Math.floor(minutes / 60);
|
|
91
|
+
if (hours < 24) return `${hours}h ${minutes % 60}m`;
|
|
92
|
+
return `${Math.floor(hours / 24)}d ${hours % 24}h`;
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cofluxd",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Coflux
|
|
3
|
+
"version": "2.16.0",
|
|
4
|
+
"description": "Run a headless Coflux device (cofluxd) and work with Coflux terminals, workspaces and your account (coflux)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cofluxd": "cofluxd.mjs",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"cofluxd.mjs",
|
|
12
12
|
"coflux.mjs",
|
|
13
13
|
"account-client.mjs",
|
|
14
|
+
"output.mjs",
|
|
14
15
|
"release-pubkey.hex",
|
|
15
16
|
"release-trust.mjs",
|
|
16
17
|
"service-unit.mjs",
|
package/release-trust.mjs
CHANGED
|
@@ -14,6 +14,18 @@ export const SUPERVISOR_RELEASE_STATEMENT_DOMAIN = Buffer.from(
|
|
|
14
14
|
|
|
15
15
|
export const CLI_RELEASE_STATEMENT_DOMAIN = Buffer.from("coflux-cli-release-v1\0", "utf8");
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The runtime and the launcher (plan 20261002-runtime-launcher-merge) each have their own domain,
|
|
19
|
+
* distinct from `coflux-worker-release-v1` / `coflux-supervisor-release-v1`: a runtime statement
|
|
20
|
+
* never verifies against the worker transcript built from identical metadata, so no pre-plan
|
|
21
|
+
* supervisor can accept a runtime artifact as a worker. Runtime artifacts carry no legacy raw
|
|
22
|
+
* signature at all.
|
|
23
|
+
*/
|
|
24
|
+
export const RUNTIME_RELEASE_STATEMENT_DOMAIN = Buffer.from("coflux-runtime-release-v1\0", "utf8");
|
|
25
|
+
export const LAUNCHER_RELEASE_STATEMENT_DOMAIN = Buffer.from("coflux-launcher-release-v1\0", "utf8");
|
|
26
|
+
export function runtimeReleaseStatement(metadata) { return artifactReleaseStatement(RUNTIME_RELEASE_STATEMENT_DOMAIN, metadata); }
|
|
27
|
+
export function launcherReleaseStatement(metadata) { return artifactReleaseStatement(LAUNCHER_RELEASE_STATEMENT_DOMAIN, metadata); }
|
|
28
|
+
|
|
17
29
|
export const TRANSPORT_RELEASE_STATEMENT_DOMAIN = Buffer.from("coflux-transport-release-v1\0", "utf8");
|
|
18
30
|
export function transportReleaseStatement(metadata) { return artifactReleaseStatement(TRANSPORT_RELEASE_STATEMENT_DOMAIN, metadata); }
|
|
19
31
|
|
|
@@ -21,15 +33,20 @@ export function transportReleaseStatement(metadata) { return artifactReleaseStat
|
|
|
21
33
|
export const PTYD_RELEASE_STATEMENT_DOMAIN = Buffer.from("coflux-ptyd-release-v1\0", "utf8");
|
|
22
34
|
export function ptydReleaseStatement(metadata) { return artifactReleaseStatement(PTYD_RELEASE_STATEMENT_DOMAIN, metadata); }
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
/** Components of a schema 3 manifest. `worker` / `supervisor` are schema 2 only. */
|
|
37
|
+
export const RELEASE_COMPONENTS = ["runtime", "launcher", "cli", "transport", "ptyd"];
|
|
38
|
+
export const RELEASE_MANIFEST_SCHEMA_VERSION = 3;
|
|
25
39
|
|
|
26
40
|
function releaseStatementFor(component, metadata) {
|
|
27
41
|
switch (component) {
|
|
42
|
+
case "runtime": return runtimeReleaseStatement(metadata);
|
|
43
|
+
case "launcher": return launcherReleaseStatement(metadata);
|
|
28
44
|
case "worker": return workerReleaseStatement(metadata);
|
|
45
|
+
case "supervisor": return supervisorReleaseStatement(metadata);
|
|
29
46
|
case "cli": return cliReleaseStatement(metadata);
|
|
30
47
|
case "transport": return transportReleaseStatement(metadata);
|
|
31
48
|
case "ptyd": return ptydReleaseStatement(metadata);
|
|
32
|
-
default:
|
|
49
|
+
default: throw new Error(`Unknown release component: ${JSON.stringify(component)}`);
|
|
33
50
|
}
|
|
34
51
|
}
|
|
35
52
|
|
|
@@ -41,11 +58,11 @@ export const MAX_RELEASE_ARTIFACT_BYTES = 128 * 1024 * 1024;
|
|
|
41
58
|
|
|
42
59
|
function parseReleaseVersion(version) {
|
|
43
60
|
const match = typeof version === "string" ? STRICT_RELEASE_VERSION.exec(version) : null;
|
|
44
|
-
if (!match) throw new Error(`
|
|
61
|
+
if (!match) throw new Error(`Release version must be strict SemVer with a v prefix: ${JSON.stringify(version)}`);
|
|
45
62
|
const prerelease = match[4]
|
|
46
63
|
? match[4].split(".").map((identifier) => {
|
|
47
64
|
if (/^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0")) {
|
|
48
|
-
throw new Error(`
|
|
65
|
+
throw new Error(`Release version has a numeric prerelease identifier with a leading zero: ${JSON.stringify(version)}`);
|
|
49
66
|
}
|
|
50
67
|
return /^\d+$/.test(identifier)
|
|
51
68
|
? { numeric: true, value: BigInt(identifier) }
|
|
@@ -104,13 +121,13 @@ function lenPrefixed(value) {
|
|
|
104
121
|
function artifactReleaseStatement(domain, { version, target, sha256, size }) {
|
|
105
122
|
assertReleaseVersion(version);
|
|
106
123
|
if (typeof target !== "string" || !target || Buffer.byteLength(target) > 128) {
|
|
107
|
-
throw new Error("
|
|
124
|
+
throw new Error("Release target is invalid");
|
|
108
125
|
}
|
|
109
126
|
if (typeof sha256 !== "string" || !SHA256_HEX.test(sha256)) {
|
|
110
|
-
throw new Error("
|
|
127
|
+
throw new Error("Release sha256 must be 32 bytes of hex");
|
|
111
128
|
}
|
|
112
129
|
if (!Number.isSafeInteger(size) || size <= 0 || size > MAX_RELEASE_ARTIFACT_BYTES) {
|
|
113
|
-
throw new Error("
|
|
130
|
+
throw new Error("Release size must be a positive integer within bounds");
|
|
114
131
|
}
|
|
115
132
|
const sizeBytes = Buffer.allocUnsafe(8);
|
|
116
133
|
sizeBytes.writeBigUInt64BE(BigInt(size));
|
|
@@ -140,7 +157,7 @@ export function cliReleaseStatement(metadata) {
|
|
|
140
157
|
export function createReleasePublicKey(publicKeyHex) {
|
|
141
158
|
const normalized = typeof publicKeyHex === "string" ? publicKeyHex.trim() : "";
|
|
142
159
|
if (!ED25519_PUBLIC_KEY_HEX.test(normalized)) {
|
|
143
|
-
throw new Error("
|
|
160
|
+
throw new Error("The release public key must be 32 bytes of hex");
|
|
144
161
|
}
|
|
145
162
|
return crypto.createPublicKey({
|
|
146
163
|
format: "jwk",
|
|
@@ -157,21 +174,23 @@ function isRecord(value) {
|
|
|
157
174
|
}
|
|
158
175
|
|
|
159
176
|
/**
|
|
160
|
-
* 从 schema
|
|
177
|
+
* 从 schema 3 manifest 取指定 component/target。额外顶层字段允许滚动扩展;
|
|
161
178
|
* 但参与信任裁决的 version/target/size/hash/signature 全部严格校验。
|
|
179
|
+
* A schema 2 manifest (worker/supervisor) is refused outright: this cofluxd installs a launcher
|
|
180
|
+
* and a runtime, and a `worker` lookup in a schema 3 manifest throws.
|
|
162
181
|
*/
|
|
163
182
|
export function parseReleaseManifestEntry(manifest, component, version, target) {
|
|
164
183
|
assertReleaseVersion(version);
|
|
165
184
|
if (!RELEASE_COMPONENTS.includes(component)) {
|
|
166
|
-
throw new Error(
|
|
185
|
+
throw new Error(`Unknown release component: ${JSON.stringify(component)}`);
|
|
167
186
|
}
|
|
168
|
-
if (!isRecord(manifest) || manifest.schemaVersion !==
|
|
169
|
-
throw new Error("
|
|
187
|
+
if (!isRecord(manifest) || manifest.schemaVersion !== RELEASE_MANIFEST_SCHEMA_VERSION || manifest.version !== version) {
|
|
188
|
+
throw new Error("Release manifest schema/version does not match the request");
|
|
170
189
|
}
|
|
171
190
|
const entries = manifest[component];
|
|
172
191
|
const entry = isRecord(entries) ? entries[target] : undefined;
|
|
173
192
|
if (!isRecord(entry) || entry.target !== target) {
|
|
174
|
-
throw new Error(`
|
|
193
|
+
throw new Error(`Release manifest has no matching ${component}/${target}`);
|
|
175
194
|
}
|
|
176
195
|
if (
|
|
177
196
|
typeof entry.sha256 !== "string" ||
|
|
@@ -182,43 +201,32 @@ export function parseReleaseManifestEntry(manifest, component, version, target)
|
|
|
182
201
|
typeof entry.releaseSignature !== "string" ||
|
|
183
202
|
!ED25519_SIGNATURE_HEX.test(entry.releaseSignature)
|
|
184
203
|
) {
|
|
185
|
-
throw new Error(`
|
|
186
|
-
}
|
|
187
|
-
if (
|
|
188
|
-
component === "worker" &&
|
|
189
|
-
(typeof entry.signature !== "string" || !ED25519_SIGNATURE_HEX.test(entry.signature))
|
|
190
|
-
) {
|
|
191
|
-
throw new Error(`release manifest 的 worker/${target} 缺少 legacy 签名`);
|
|
204
|
+
throw new Error(`Release manifest metadata is invalid for ${component}/${target}`);
|
|
192
205
|
}
|
|
206
|
+
// No component of schema 3 carries a legacy raw-binary signature (the worker's was the back
|
|
207
|
+
// door old supervisors still check); a stray `signature` field is ignored, never trusted.
|
|
193
208
|
return {
|
|
194
209
|
target,
|
|
195
210
|
sha256: entry.sha256.toLowerCase(),
|
|
196
211
|
size: entry.size,
|
|
197
|
-
signature: component === "worker" ? entry.signature.toLowerCase() : undefined,
|
|
198
212
|
releaseSignature: entry.releaseSignature.toLowerCase(),
|
|
199
213
|
};
|
|
200
214
|
}
|
|
201
215
|
|
|
202
|
-
/** 校验实际 bytes 与 manifest
|
|
216
|
+
/** 校验实际 bytes 与 manifest 元数据及 component-separated release 签名。 */
|
|
203
217
|
export function verifyReleaseArtifact({ component, version, entry, data, publicKey }) {
|
|
204
|
-
if (!Buffer.isBuffer(data)) throw new Error("
|
|
218
|
+
if (!Buffer.isBuffer(data)) throw new Error("Release artifact must be a Buffer");
|
|
205
219
|
if (data.byteLength !== entry.size) {
|
|
206
|
-
throw new Error(`${component}
|
|
220
|
+
throw new Error(`${component} artifact size does not match: expected ${entry.size}, got ${data.byteLength}`);
|
|
207
221
|
}
|
|
208
222
|
const sha256 = crypto.createHash("sha256").update(data).digest("hex");
|
|
209
223
|
if (sha256 !== entry.sha256) {
|
|
210
|
-
throw new Error(`${component}
|
|
211
|
-
}
|
|
212
|
-
if (
|
|
213
|
-
component === "worker" &&
|
|
214
|
-
!crypto.verify(null, data, publicKey, Buffer.from(entry.signature, "hex"))
|
|
215
|
-
) {
|
|
216
|
-
throw new Error("worker 产物 legacy Ed25519 签名无效");
|
|
224
|
+
throw new Error(`${component} artifact sha256 does not match`);
|
|
217
225
|
}
|
|
218
226
|
const metadata = { version, target: entry.target, sha256, size: data.byteLength };
|
|
219
227
|
const statement = releaseStatementFor(component, metadata);
|
|
220
228
|
if (!crypto.verify(null, statement, publicKey, Buffer.from(entry.releaseSignature, "hex"))) {
|
|
221
|
-
throw new Error(`${component}
|
|
229
|
+
throw new Error(`${component} artifact release Ed25519 signature is invalid`);
|
|
222
230
|
}
|
|
223
231
|
}
|
|
224
232
|
|
|
@@ -257,16 +265,16 @@ export function installStagedPair(staged) {
|
|
|
257
265
|
catch (restoreError) { restoreFailures.push(restoreError); }
|
|
258
266
|
}
|
|
259
267
|
if (restoreFailures.length > 0) {
|
|
260
|
-
throw new AggregateError([error, ...restoreFailures], "
|
|
268
|
+
throw new AggregateError([error, ...restoreFailures], "Replacing the binaries failed and the previous version could not be fully restored");
|
|
261
269
|
}
|
|
262
270
|
throw error;
|
|
263
271
|
}
|
|
264
272
|
}
|
|
265
273
|
|
|
266
|
-
/** Publish a complete native
|
|
267
|
-
* A
|
|
274
|
+
/** Publish a complete native release before changing any executable entry point.
|
|
275
|
+
* A runtime reached through a bin symlink resolves current_exe into its immutable
|
|
268
276
|
* release directory, so interruption between entry-point updates cannot pair
|
|
269
|
-
* that
|
|
277
|
+
* that runtime with a helper from another release. */
|
|
270
278
|
export function installNativeRelease(staged) {
|
|
271
279
|
if (!staged.some(({ destination }) => path.basename(destination) === "coflux-transport")) return installStagedPair(staged);
|
|
272
280
|
const binDir = path.dirname(staged[0].destination);
|
package/service-unit.mjs
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
|
-
/** The two variables the
|
|
11
|
-
* `crates/
|
|
10
|
+
/** The two variables the runtime reads; kept identical to `packages/executor/src/env.ts` and
|
|
11
|
+
* `crates/runtime/src/executor_host.rs`. */
|
|
12
12
|
export const EXECUTOR_NODE_ENV = "COFLUX_EXECUTOR_NODE";
|
|
13
13
|
export const EXECUTOR_ENTRY_ENV = "COFLUX_EXECUTOR_ENTRY";
|
|
14
14
|
|
|
@@ -52,7 +52,7 @@ function oneLine(value) {
|
|
|
52
52
|
return !/[\n\r]/.test(String(value));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function plistXml({
|
|
55
|
+
export function plistXml({ launcherBin, home, logFile, executor }) {
|
|
56
56
|
const variables = [["COFLUX_HOME", home]];
|
|
57
57
|
if (executor) {
|
|
58
58
|
variables.push([EXECUTOR_NODE_ENV, executor.node], [EXECUTOR_ENTRY_ENV, executor.entry]);
|
|
@@ -66,7 +66,7 @@ export function plistXml({ supervisorBin, home, logFile, executor }) {
|
|
|
66
66
|
<dict>
|
|
67
67
|
<key>Label</key><string>com.coflux.daemon</string>
|
|
68
68
|
<key>ProgramArguments</key>
|
|
69
|
-
<array><string>${xml(
|
|
69
|
+
<array><string>${xml(launcherBin)}</string></array>
|
|
70
70
|
<key>EnvironmentVariables</key>
|
|
71
71
|
<dict>
|
|
72
72
|
${entries}
|
|
@@ -81,9 +81,9 @@ ${entries}
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* ptyd (plan 20260918-ptyd-terminal-custody) runs under its own label: the
|
|
85
|
-
* leaves sessions in ptyd, so `launchctl unload/load com.coflux.daemon` touches only the
|
|
86
|
-
* and KeepAlive brings ptyd back on its own if it crashes.
|
|
84
|
+
* ptyd (plan 20260918-ptyd-terminal-custody) runs under its own label: the launcher's SIGTERM
|
|
85
|
+
* leaves sessions in ptyd, so `launchctl unload/load com.coflux.daemon` touches only the launcher
|
|
86
|
+
* and its runtime, and KeepAlive brings ptyd back on its own if it crashes.
|
|
87
87
|
*/
|
|
88
88
|
export function ptydPlistXml({ ptydBin, home, logFile }) {
|
|
89
89
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -107,9 +107,9 @@ export function ptydPlistXml({ ptydBin, home, logFile }) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* systemd: ptyd has its own unit and the
|
|
111
|
-
* coflux-daemon` restarts only the
|
|
112
|
-
* untouched. Stopping ptyd (down) also stops the
|
|
110
|
+
* systemd: ptyd has its own unit and the launcher's unit Requires/After it. `systemctl restart
|
|
111
|
+
* coflux-daemon` restarts only the launcher's cgroup (runtime included); ptyd's is not part of it,
|
|
112
|
+
* so terminals are untouched. Stopping ptyd (down) also stops the launcher (Requires).
|
|
113
113
|
*/
|
|
114
114
|
export function ptydSystemdUnit({ ptydBin, home }) {
|
|
115
115
|
const environment = oneLine(home) ? `Environment=COFLUX_HOME=${home}\n` : "";
|
|
@@ -127,7 +127,7 @@ WantedBy=default.target
|
|
|
127
127
|
`;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
export function systemdUnit({
|
|
130
|
+
export function systemdUnit({ launcherBin, home, executor }) {
|
|
131
131
|
const variables = [["COFLUX_HOME", home]];
|
|
132
132
|
if (executor) {
|
|
133
133
|
variables.push([EXECUTOR_NODE_ENV, executor.node], [EXECUTOR_ENTRY_ENV, executor.entry]);
|
|
@@ -137,14 +137,14 @@ export function systemdUnit({ supervisorBin, home, executor }) {
|
|
|
137
137
|
.map(([key, value]) => `Environment=${key}=${value}`)
|
|
138
138
|
.join("\n");
|
|
139
139
|
return `[Unit]
|
|
140
|
-
Description=coflux daemon (
|
|
140
|
+
Description=coflux daemon (launcher + runtime)
|
|
141
141
|
After=network-online.target coflux-ptyd.service
|
|
142
142
|
Wants=network-online.target
|
|
143
143
|
Requires=coflux-ptyd.service
|
|
144
144
|
|
|
145
145
|
[Service]
|
|
146
146
|
${environment}
|
|
147
|
-
ExecStart=${
|
|
147
|
+
ExecStart=${launcherBin}
|
|
148
148
|
Restart=always
|
|
149
149
|
RestartSec=2
|
|
150
150
|
|
|
@@ -152,3 +152,42 @@ RestartSec=2
|
|
|
152
152
|
WantedBy=default.target
|
|
153
153
|
`;
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Operator and test overrides the self-managed service keeps from the environment of
|
|
158
|
+
* `cofluxd up` / `cofluxd run`: the release public key (tests and self-keyed releases), the
|
|
159
|
+
* runtime's probation window, and its connection timing knobs. Nothing a Coflux terminal sets.
|
|
160
|
+
*/
|
|
161
|
+
export const WATCHER_ENV_ALLOWLIST = [
|
|
162
|
+
"COFLUX_WORKER_PUBKEY",
|
|
163
|
+
"COFLUX_RUNTIME_PROBATION_MS",
|
|
164
|
+
"COFLUX_CONNECT_TIMEOUT_MS",
|
|
165
|
+
"COFLUX_IDLE_PING_MS",
|
|
166
|
+
"COFLUX_IDLE_GRACE_MS",
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The environment of the self-managed service (`coflux-launcher watch`): the same `COFLUX_*`
|
|
171
|
+
* variables the launchd / systemd units carry, and only those.
|
|
172
|
+
*
|
|
173
|
+
* Not `{ ...process.env, COFLUX_HOME }`: `cofluxd up` is often run from inside a Coflux terminal,
|
|
174
|
+
* whose environment is a copy of the running service's own — `COFLUX_LAUNCHER_SOCK`,
|
|
175
|
+
* `COFLUX_LAUNCHER_NONCE`, `COFLUX_TRANSPORT_*`, `COFLUX_LOCAL_GATEWAY_PORT`, `COFLUX_SESSION_ID` and,
|
|
176
|
+
* under the desktop app, `COFLUX_RUNTIME_CMD`. The launcher and runtime read those and would bind
|
|
177
|
+
* to another service's sockets. Every other `COFLUX_*` variable is dropped except the allowlist;
|
|
178
|
+
* everything else (PATH, HOME, locale) passes through.
|
|
179
|
+
*/
|
|
180
|
+
export function watcherEnv(base, { home, executor }) {
|
|
181
|
+
const env = {};
|
|
182
|
+
for (const [key, value] of Object.entries(base)) {
|
|
183
|
+
if (value === undefined) continue;
|
|
184
|
+
if (key.startsWith("COFLUX_") && !WATCHER_ENV_ALLOWLIST.includes(key)) continue;
|
|
185
|
+
env[key] = value;
|
|
186
|
+
}
|
|
187
|
+
env.COFLUX_HOME = home;
|
|
188
|
+
if (executor) {
|
|
189
|
+
env[EXECUTOR_NODE_ENV] = executor.node;
|
|
190
|
+
env[EXECUTOR_ENTRY_ENV] = executor.entry;
|
|
191
|
+
}
|
|
192
|
+
return env;
|
|
193
|
+
}
|
package/skills/coflux/SKILL.md
CHANGED
|
@@ -276,7 +276,7 @@ command line is capped at 64 KB. Always write `--cmd=<value>` and `--title=<valu
|
|
|
276
276
|
never separated by a space: a value that starts with `-` is otherwise taken for another option.
|
|
277
277
|
|
|
278
278
|
`new` prints the terminal id on its first line whatever happens next; when the command could not
|
|
279
|
-
be typed it says so
|
|
279
|
+
be typed it says so in an error on stderr (see "Errors").
|
|
280
280
|
|
|
281
281
|
The new terminal has the same `COFLUX_*` variables (pointing at its own task/session ids, same
|
|
282
282
|
workspace as you).
|
|
@@ -537,7 +537,9 @@ this machine, so they are there whether or not a desktop is open.
|
|
|
537
537
|
|
|
538
538
|
### Errors from local commands
|
|
539
539
|
|
|
540
|
-
Errors
|
|
540
|
+
Errors go to stderr as `✗ Error: <what happened>` followed by an indented line saying what to do
|
|
541
|
+
next; the sentences below come from the daemon and appear after `Error:`. Do what they say: "not
|
|
542
|
+
inside a coflux terminal" = you are not
|
|
541
543
|
in a coflux session; "terminal is not in this workspace or does not exist" = check the id with
|
|
542
544
|
`list`, and if you moved into another workspace that is exactly what a terminal of the other one
|
|
543
545
|
looks like (`coflux workspace` to confirm, `cd` back to reach it); "predates the daemon upgrade" =
|
|
@@ -635,8 +637,8 @@ CLI's exit code is the remote command's**, so an ordinary shell test around it w
|
|
|
635
637
|
failures — device offline (it exists and is not connected), no device of this account with that id
|
|
636
638
|
(`设备 <id> 不存在或不属于当前账号`: a mistyped or bare-prefix id, **not** an outage to investigate),
|
|
637
639
|
that device's daemon too old (`cofluxd update && cofluxd restart` there), `--cwd` missing or not a
|
|
638
|
-
directory, the timeout elapsing — are
|
|
639
|
-
never confused with "it never ran".
|
|
640
|
+
directory, the timeout elapsing — are reported as `✗ Error:` with a next step and exit **255**, so a
|
|
641
|
+
remote exit 1 is never confused with "it never ran".
|
|
640
642
|
|
|
641
643
|
`--cwd` is the only addressing: an absolute path or a `~` prefix, defaulting to the daemon user's
|
|
642
644
|
HOME. To run inside a workspace, pass the `path` from `coflux workspace list --device <deviceId>`.
|