c8ctl-plugin-nano 1.44.5 → 1.44.6

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 CHANGED
@@ -262,6 +262,19 @@ specialisation.
262
262
  > (capability-declared) enrolment path. Use `--auto-scope <process-id | prefix>`
263
263
  > to narrow the blast radius to one app/network.
264
264
 
265
+ > **Dual-stack / IPv6-first engine hosts.** The engine client races IPv4 and IPv6
266
+ > (Happy-Eyeballs, RFC 8305) at connect time, so a worker whose engine host
267
+ > resolves to an **unreachable IPv6 address first** — common with macOS mDNS
268
+ > (`merlin.local → fe80::… (dead) → 192.168.x.x`), a stray/misordered `AAAA`, or
269
+ > an IPv6-advertised host — transparently falls back to IPv4 instead of failing
270
+ > the activation / `--auto` engine read (`fetch failed` / `write EPIPE` /
271
+ > `UND_ERR_CONNECT_TIMEOUT`). Without this, the `--auto` autoscaler would read
272
+ > `0` job types and **silently scale the fleet to zero** (workers vanish from the
273
+ > Nano console) even though `curl`/`fetch` reach the same host fine
274
+ > (jwulf/c8ctl-plugin-nano#139). No configuration is needed; if you still want to
275
+ > pin a family you can set `NODE_OPTIONS=--dns-result-order=ipv4first` or point
276
+ > the engine URL at the IPv4 literal.
277
+
265
278
 
266
279
  The optional `--name` sets **this worker's name** — the `workerName` it
267
280
  registers under at the broker (`‹name›:‹jobType›`) and how it shows up in
package/c8ctl-plugin.js CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  unwatchFile,
52
52
  } from 'node:fs';
53
53
  import { createConnection, createServer } from 'node:net';
54
+ import * as nodeNet from 'node:net';
54
55
  import { lookup as dnsLookup } from 'node:dns/promises';
55
56
  import { randomUUID, createHash, randomBytes } from 'node:crypto';
56
57
  import { homedir, platform as osPlatform, devNull, tmpdir, hostname } from 'node:os';
@@ -6045,6 +6046,58 @@ function buildActivityPayload({ pid, updatedAt, jobs, engine, agentic }) {
6045
6046
  };
6046
6047
  }
6047
6048
 
6049
+ /**
6050
+ * Enable Happy-Eyeballs (RFC 8305) dual-stack connect for the engine client so a
6051
+ * worker whose engine host resolves IPv6-first with an *unreachable* v6 address
6052
+ * (macOS mDNS `fe80::` link-local, a stray/misordered AAAA, an IPv6-advertised
6053
+ * host) transparently races and falls back to IPv4 — matching `curl`/`fetch` —
6054
+ * instead of connecting to the dead address and failing the activation / `--auto`
6055
+ * engine read with `fetch failed` / `write EPIPE` / `UND_ERR_CONNECT_TIMEOUT`
6056
+ * (jwulf/c8ctl-plugin-nano#139).
6057
+ *
6058
+ * We set Node's PROCESS-WIDE `net` default rather than an instance option so the
6059
+ * fix covers the *class*, not one call site: BOTH transports the worker harness
6060
+ * uses inherit it — the `@camunda8` SDK client's `activateJobs` (its undici
6061
+ * dispatcher connects via `net`) AND the raw-`fetch` `--auto` reconcile / initial
6062
+ * engine reads. Node ≥20 already defaults this on, but older runtimes — and any
6063
+ * build where the client's dispatcher inherits a `false` default — leave it off;
6064
+ * the reporter's repro shows the failing "SDK default path" is exactly
6065
+ * `setDefaultAutoSelectFamily(false)`, and asserting `true` here is what fixes it.
6066
+ * The attempt timeout bounds the per-address race so a dead `fe80::` yields to
6067
+ * IPv4 quickly (RFC 8305 default of 250ms) instead of stalling the connect.
6068
+ *
6069
+ * Fail-open and idempotent: a runtime without the API (or any throw) is swallowed
6070
+ * so Happy-Eyeballs — an optimisation — can never block a worker from starting.
6071
+ *
6072
+ * @param {{ net?: object, attemptTimeoutMs?: number }} [opts] injection seam for tests
6073
+ * @returns {boolean} true if the net default was (re)asserted to Happy-Eyeballs
6074
+ */
6075
+ function enableEngineHappyEyeballs(opts = {}) {
6076
+ try {
6077
+ // Destructure INSIDE the try so a non-object arg (e.g. `null`) fails open
6078
+ // like any other throw rather than blowing up before the guard.
6079
+ const { net = nodeNet, attemptTimeoutMs = 250 } = opts || {};
6080
+ if (net && typeof net.setDefaultAutoSelectFamily === 'function') {
6081
+ net.setDefaultAutoSelectFamily(true);
6082
+ // The attempt timeout is an optional refinement: once the primary family
6083
+ // default is asserted the contract is satisfied, so a throw here must not
6084
+ // retract our `true`. Swallow it independently.
6085
+ try {
6086
+ if (typeof net.setDefaultAutoSelectFamilyAttemptTimeout === 'function'
6087
+ && Number.isFinite(attemptTimeoutMs) && attemptTimeoutMs > 0) {
6088
+ net.setDefaultAutoSelectFamilyAttemptTimeout(attemptTimeoutMs);
6089
+ }
6090
+ } catch {
6091
+ // Fail-open on the optional timeout setter; the family default still holds.
6092
+ }
6093
+ return true;
6094
+ }
6095
+ } catch {
6096
+ // Fail-open: Happy-Eyeballs is a connectivity optimisation, never a start gate.
6097
+ }
6098
+ return false;
6099
+ }
6100
+
6048
6101
  /**
6049
6102
  * work — turn a hire profile into live Nano job workers (one per job-type in
6050
6103
  * the rank×capability matrix) and poll for work in the foreground until Ctrl-C.
@@ -6279,6 +6332,12 @@ async function workAgent(req, flags) {
6279
6332
  logger.error('--auto-scope requires --auto (it narrows the engine-read agent job types).');
6280
6333
  process.exit(1);
6281
6334
  }
6335
+ // Enable Happy-Eyeballs (RFC 8305) at the socket layer BEFORE the SDK client
6336
+ // or any engine read is created, so a worker whose engine host resolves
6337
+ // IPv6-first with a dead v6 address falls back to IPv4 instead of silently
6338
+ // scaling the fleet to zero (jwulf/c8ctl-plugin-nano#139). Process-wide, so it
6339
+ // covers both the SDK's activateJobs and the raw-fetch --auto engine reads.
6340
+ enableEngineHappyEyeballs();
6282
6341
  const camunda = globalThis.c8ctl.createClient();
6283
6342
 
6284
6343
  // Broker REST endpoint for live linked-resource prompts (issue #63) and the
@@ -11612,6 +11671,7 @@ export {
11612
11671
  serviceTaskHasAgentHeader,
11613
11672
  readDeployedAgentJobTypes,
11614
11673
  resolveAutoJobTypes,
11674
+ enableEngineHappyEyeballs,
11615
11675
  workAgent,
11616
11676
  derivePollTimeoutMs,
11617
11677
  AGENT_TASK_NS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.44.5",
3
+ "version": "1.44.6",
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,12 +57,12 @@
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "node-pty": "^1.0.0",
60
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.5",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.5",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.5",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.5",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.5",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.5",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.5"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.6",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.6",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.6",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.6",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.6",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.6",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.6"
67
67
  }
68
68
  }