c8ctl-plugin-nano 1.56.2 → 1.56.3
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 +36 -0
- package/c8ctl-plugin.js +112 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1033,6 +1033,42 @@ it cannot (e.g. `launchctl` is unavailable, or you set `C8CTL_NANO_NO_LAUNCHD=1`
|
|
|
1033
1033
|
it prints a prominent **warning** pointing at `supervisor install` instead of
|
|
1034
1034
|
silently leaving a fleet that will wedge on logout.
|
|
1035
1035
|
|
|
1036
|
+
> **macOS Local Network Privacy + a LAN engine (macOS 15 Sequoia / 26 Tahoe).**
|
|
1037
|
+
> The `gui/$UID` LaunchAgent that keeps the fleet alive across SSH logout is a
|
|
1038
|
+
> **distinct TCC identity** — and on macOS 15+ it is **not** granted **Local
|
|
1039
|
+
> Network** access, which an interactive SSH/Terminal session inherits for free.
|
|
1040
|
+
> So a service-owned fleet whose engine is on the **LAN** (`merlin.local`,
|
|
1041
|
+
> `192.168.x.x`, `10.x`, …) hits the **same wedge** as the logout case —
|
|
1042
|
+
> `activateJobs failed: fetch failed`, `reconcile skipped — fetch failed`,
|
|
1043
|
+
> `listening on 0 job type(s)`, agentic `disconnected` — **even while you are
|
|
1044
|
+
> still logged in**. The tell is `EHOSTUNREACH` (or a silent connect failure) to
|
|
1045
|
+
> the engine's **LAN IP** from the service, while the *same* fetch works from an
|
|
1046
|
+
> interactive SSH shell and **internet-reachable** hosts still work from the
|
|
1047
|
+
> service. This is *not* the IPv6/mDNS case above — it fails even against the raw
|
|
1048
|
+
> IPv4 literal, because the OS is blocking LAN egress for the launchd identity.
|
|
1049
|
+
>
|
|
1050
|
+
> There is **no `tccutil` / CLI grant** for a headless launchd tool. Pick one:
|
|
1051
|
+
> - **Grant the launch program Local Network access (simplest).** On the Mac's
|
|
1052
|
+
> GUI, **System Settings → Privacy & Security → Local Network**, enable the
|
|
1053
|
+
> entry for the service's program — the **Node.js** runtime (it may surface as
|
|
1054
|
+
> *“Node.js Foundation”* / *“App Background Activity”*). Then
|
|
1055
|
+
> `c8ctl nano supervisor stop && c8ctl nano supervisor start`. The grant is
|
|
1056
|
+
> keyed to the node binary, so it persists across restarts.
|
|
1057
|
+
> - **Route over Tailscale (keeps logout survival, no LAN grant needed).** Traffic
|
|
1058
|
+
> over the Tailscale `utun` interface is **not** classified as “local network”,
|
|
1059
|
+
> so the launchd service reaches it fine. Point the engine at the tailnet
|
|
1060
|
+
> address (e.g. `NANO_REST_URL=http://<host>.<tailnet>.ts.net:8080`, or bake it
|
|
1061
|
+
> into the active profile so the service inherits it).
|
|
1062
|
+
> - **Run in the SSH session instead** (`supervisor uninstall`) — inherits the
|
|
1063
|
+
> Terminal grant, but reverts to dying on logout unless you pin a `tmux`/SSH
|
|
1064
|
+
> session. Note that on macOS `supervisor start` over SSH **auto-reparents**
|
|
1065
|
+
> back into the `gui/$UID` launchd identity (reinstalling the LaunchAgent)
|
|
1066
|
+
> unless you set `C8CTL_NANO_NO_LAUNCHD=1` — without that opt-out you land in
|
|
1067
|
+
> the same blocked identity. Start from a **local Terminal** session (or export
|
|
1068
|
+
> `C8CTL_NANO_NO_LAUNCHD=1`) to keep the interactive grant.
|
|
1069
|
+
> - **Run the daemon as root** — root retains LAN access, but running agent
|
|
1070
|
+
> harnesses as root is a poor trade; prefer the options above.
|
|
1071
|
+
|
|
1036
1072
|
## Composing a workforce: `workforce`
|
|
1037
1073
|
|
|
1038
1074
|
`supervisor` is imperative — you compose a fleet with a `start --worker …` plus a
|
package/c8ctl-plugin.js
CHANGED
|
@@ -6896,6 +6896,101 @@ function withIpv4FirstNodeOptions(env) {
|
|
|
6896
6896
|
return { ...base, NODE_OPTIONS: ipv4FirstNodeOptions(base.NODE_OPTIONS) };
|
|
6897
6897
|
}
|
|
6898
6898
|
|
|
6899
|
+
// The reverse-DNS label prefix of the session-independent supervisor LaunchAgent
|
|
6900
|
+
// (macOS). A daemon/worker spawned by that service inherits `XPC_SERVICE_NAME`
|
|
6901
|
+
// set to `<prefix><stateHomeHash>`, so its presence is a reliable "am I running
|
|
6902
|
+
// under the launchd `gui/$UID` service (not an interactive SSH/Terminal
|
|
6903
|
+
// session)?" signal — the axis that decides whether macOS Local Network Privacy
|
|
6904
|
+
// applies. `supervisorServiceLabel()` composes the full label from this prefix.
|
|
6905
|
+
const SUPERVISOR_SERVICE_LABEL_PREFIX = 'io.nanobpm.c8ctl-nano.supervisor.';
|
|
6906
|
+
|
|
6907
|
+
/**
|
|
6908
|
+
* True when THIS process is running under the supervisor's launchd LaunchAgent
|
|
6909
|
+
* (the `gui/$UID` service, macOS). Keyed off the inherited `XPC_SERVICE_NAME`.
|
|
6910
|
+
* @param {Record<string,string|undefined>} [env]
|
|
6911
|
+
*/
|
|
6912
|
+
function isUnderLaunchdSupervisorService(env = process.env) {
|
|
6913
|
+
const xpc = env?.XPC_SERVICE_NAME;
|
|
6914
|
+
return typeof xpc === 'string' && xpc.startsWith(SUPERVISOR_SERVICE_LABEL_PREFIX);
|
|
6915
|
+
}
|
|
6916
|
+
|
|
6917
|
+
/**
|
|
6918
|
+
* True when `address` is a private (RFC1918) or link-local IPv4 — i.e. a *local
|
|
6919
|
+
* network* peer, the class macOS 15+ Local Network Privacy gates. Tailscale's
|
|
6920
|
+
* CGNAT range (100.64.0.0/10) is deliberately EXCLUDED: it rides the `utun`
|
|
6921
|
+
* tunnel, which the OS does not treat as "local network", so it is not blocked
|
|
6922
|
+
* and must not trip the heuristic. Loopback and public addresses are excluded.
|
|
6923
|
+
* @param {unknown} address
|
|
6924
|
+
*/
|
|
6925
|
+
function isPrivateLanIPv4(address) {
|
|
6926
|
+
if (typeof address !== 'string') return false;
|
|
6927
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(address);
|
|
6928
|
+
if (!m) return false;
|
|
6929
|
+
const a = Number(m[1]);
|
|
6930
|
+
const b = Number(m[2]);
|
|
6931
|
+
if (a > 255 || b > 255 || Number(m[3]) > 255 || Number(m[4]) > 255) return false;
|
|
6932
|
+
if (a === 10) return true; // 10.0.0.0/8
|
|
6933
|
+
if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
|
|
6934
|
+
if (a === 192 && b === 168) return true; // 192.168.0.0/16
|
|
6935
|
+
if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local
|
|
6936
|
+
return false;
|
|
6937
|
+
}
|
|
6938
|
+
|
|
6939
|
+
// Walk an error and its `.cause` chain (undici wraps the OS-level `code`/`address`
|
|
6940
|
+
// under `.cause`), returning the first defined value of `field`. Bounded depth so
|
|
6941
|
+
// a self-referential cause can't loop.
|
|
6942
|
+
function errorChainField(error, field, maxDepth = 6) {
|
|
6943
|
+
let e = error;
|
|
6944
|
+
for (let i = 0; i < maxDepth && e && typeof e === 'object'; i++) {
|
|
6945
|
+
if (e[field] !== undefined && e[field] !== null) return e[field];
|
|
6946
|
+
e = e.cause;
|
|
6947
|
+
}
|
|
6948
|
+
return undefined;
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
/**
|
|
6952
|
+
* Heuristic: does this engine-read / activation failure look like a macOS Local
|
|
6953
|
+
* Network Privacy (TCC) block rather than an ordinary connectivity error?
|
|
6954
|
+
*
|
|
6955
|
+
* On macOS 15 Sequoia / 26 Tahoe the supervisor's `gui/$UID` LaunchAgent is a
|
|
6956
|
+
* distinct TCC identity that is NOT granted Local Network access, so every
|
|
6957
|
+
* connection to a LAN peer fails `EHOSTUNREACH` (even the raw IPv4 literal) while
|
|
6958
|
+
* internet hosts still work — the fleet wedges with `fetch failed` / `0` job
|
|
6959
|
+
* types even though an interactive SSH shell reaches the same engine fine. The
|
|
6960
|
+
* signature that distinguishes it from the IPv6/mDNS case (#139/#151 — which
|
|
6961
|
+
* fails against a dead `fe80::…` but succeeds on IPv4) is: running under the
|
|
6962
|
+
* launchd service, `EHOSTUNREACH`, to a *private IPv4* address.
|
|
6963
|
+
*
|
|
6964
|
+
* Injection seams (`platform`, `env`) keep it unit-testable without a Mac.
|
|
6965
|
+
* @param {{ error?: unknown, platform?: string, env?: Record<string,string|undefined> }} [opts]
|
|
6966
|
+
*/
|
|
6967
|
+
function isLikelyLocalNetworkTccBlock({ error, platform = osPlatform(), env = process.env } = {}) {
|
|
6968
|
+
if (platform !== 'darwin') return false;
|
|
6969
|
+
if (!isUnderLaunchdSupervisorService(env)) return false;
|
|
6970
|
+
if (errorChainField(error, 'code') !== 'EHOSTUNREACH') return false;
|
|
6971
|
+
return isPrivateLanIPv4(errorChainField(error, 'address'));
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6974
|
+
/** Actionable multi-line hint shown when {@link isLikelyLocalNetworkTccBlock}. */
|
|
6975
|
+
function localNetworkTccHint() {
|
|
6976
|
+
return [
|
|
6977
|
+
'↳ This looks like macOS Local Network Privacy blocking the supervisor service.',
|
|
6978
|
+
' The gui/$UID LaunchAgent has no Local Network grant, so it cannot reach a LAN',
|
|
6979
|
+
' engine (an interactive SSH/Terminal session can). Common fixes (see the',
|
|
6980
|
+
' README "macOS Local Network Privacy" section for the full list, including',
|
|
6981
|
+
' running the daemon as root):',
|
|
6982
|
+
' • System Settings → Privacy & Security → Local Network → enable Node.js',
|
|
6983
|
+
' (may show as "Node.js Foundation" / "App Background Activity"), then',
|
|
6984
|
+
' `c8ctl nano supervisor stop && c8ctl nano supervisor start`.',
|
|
6985
|
+
' • Route over Tailscale (utun is exempt): point the engine at the tailnet',
|
|
6986
|
+
' address, e.g. NANO_REST_URL=http://<host>.<tailnet>.ts.net:8080',
|
|
6987
|
+
' • Or run in the SSH session (`c8ctl nano supervisor uninstall`) and pin a',
|
|
6988
|
+
' tmux/SSH session so the fleet does not die on logout. Note: over SSH,',
|
|
6989
|
+
' `supervisor start` auto-reparents back under launchd (same block) unless',
|
|
6990
|
+
' you set C8CTL_NANO_NO_LAUNCHD=1 or start from a local Terminal.',
|
|
6991
|
+
].join('\n');
|
|
6992
|
+
}
|
|
6993
|
+
|
|
6899
6994
|
/**
|
|
6900
6995
|
* work — turn a hire profile into live Nano job workers (one per job-type in
|
|
6901
6996
|
* the rank×capability matrix) and poll for work in the foreground until Ctrl-C.
|
|
@@ -7169,6 +7264,7 @@ async function workAgent(req, flags) {
|
|
|
7169
7264
|
jobTypes = [...new Set([...autoTypes, ...extraJobTypes])];
|
|
7170
7265
|
} catch (err) {
|
|
7171
7266
|
logger.warn(`--auto: initial engine read failed (${err?.message || err}); starting with no auto pollers — will retry on the next poll.`);
|
|
7267
|
+
if (isLikelyLocalNetworkTccBlock({ error: err })) logger.warn(localNetworkTccHint());
|
|
7172
7268
|
jobTypes = [...new Set(extraJobTypes)];
|
|
7173
7269
|
}
|
|
7174
7270
|
} else {
|
|
@@ -9773,6 +9869,16 @@ async function attachSupervisorConsole(state) {
|
|
|
9773
9869
|
// into `gui/$UID` (macOS) or a `systemd --user` unit with lingering (Linux),
|
|
9774
9870
|
// and `supervisor start` over SSH on macOS auto-reparents into that domain (or,
|
|
9775
9871
|
// when it cannot, WARNS that the fleet will die on logout).
|
|
9872
|
+
//
|
|
9873
|
+
// CAVEAT (macOS 15 Sequoia / 26 Tahoe — Local Network Privacy): the `gui/$UID`
|
|
9874
|
+
// LaunchAgent is a distinct TCC identity that is NOT granted Local Network
|
|
9875
|
+
// access (an interactive SSH/Terminal session inherits it for free). So a
|
|
9876
|
+
// service-owned fleet whose engine is on the LAN (`merlin.local`, `192.168.x.x`)
|
|
9877
|
+
// hits the SAME wedge — `fetch failed` / `EHOSTUNREACH` to the LAN IP, `0` job
|
|
9878
|
+
// types — even while logged in, while internet hosts still work. There is no
|
|
9879
|
+
// `tccutil`/CLI grant for a headless tool: grant the node runtime Local Network
|
|
9880
|
+
// access in System Settings, route over Tailscale (utun is exempt), or run in
|
|
9881
|
+
// the SSH session / as root. See README "Surviving SSH logout" for the full note.
|
|
9776
9882
|
// ---------------------------------------------------------------------------
|
|
9777
9883
|
|
|
9778
9884
|
/** True when the current process is running inside an SSH login session. */
|
|
@@ -9799,7 +9905,7 @@ function supervisorServiceHash() {
|
|
|
9799
9905
|
|
|
9800
9906
|
/** Reverse-DNS LaunchAgent label for this state home. */
|
|
9801
9907
|
function supervisorServiceLabel() {
|
|
9802
|
-
return
|
|
9908
|
+
return `${SUPERVISOR_SERVICE_LABEL_PREFIX}${supervisorServiceHash()}`;
|
|
9803
9909
|
}
|
|
9804
9910
|
|
|
9805
9911
|
/** Per-user LaunchAgent plist path (macOS). */
|
|
@@ -12971,6 +13077,11 @@ export {
|
|
|
12971
13077
|
createSupervisorDeps,
|
|
12972
13078
|
enableEngineHappyEyeballs,
|
|
12973
13079
|
preferIpv4Resolution,
|
|
13080
|
+
isLikelyLocalNetworkTccBlock,
|
|
13081
|
+
isUnderLaunchdSupervisorService,
|
|
13082
|
+
isPrivateLanIPv4,
|
|
13083
|
+
localNetworkTccHint,
|
|
13084
|
+
SUPERVISOR_SERVICE_LABEL_PREFIX,
|
|
12974
13085
|
ipv4FirstNodeOptions,
|
|
12975
13086
|
withIpv4FirstNodeOptions,
|
|
12976
13087
|
DNS_RESULT_ORDER_IPV4_FIRST,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.56.
|
|
3
|
+
"version": "1.56.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
|
|
6
6
|
"main": "c8ctl-plugin.js",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@commitlint/cli": "^20.4.1",
|
|
59
59
|
"@commitlint/config-conventional": "^20.4.1",
|
|
60
|
-
"@nanobpm/engine-wasm": "^0.
|
|
60
|
+
"@nanobpm/engine-wasm": "^0.9.0",
|
|
61
61
|
"@semantic-release/exec": "^7.1.0",
|
|
62
62
|
"@semantic-release/github": "^12.0.6",
|
|
63
63
|
"@types/node": "^22.20.1",
|
|
@@ -72,12 +72,12 @@
|
|
|
72
72
|
},
|
|
73
73
|
"optionalDependencies": {
|
|
74
74
|
"node-pty": "^1.0.0",
|
|
75
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.56.
|
|
76
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.56.
|
|
77
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.56.
|
|
78
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.56.
|
|
79
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.56.
|
|
80
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.56.
|
|
81
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.56.
|
|
75
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.56.3",
|
|
76
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.56.3",
|
|
77
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.56.3",
|
|
78
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.56.3",
|
|
79
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.56.3",
|
|
80
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.56.3",
|
|
81
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.56.3"
|
|
82
82
|
}
|
|
83
83
|
}
|