c8ctl-plugin-nano 1.36.1 → 1.36.2
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 +18 -13
- package/c8ctl-plugin.js +72 -30
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -292,23 +292,28 @@ c8ctl nano work reviewer
|
|
|
292
292
|
|
|
293
293
|
**Zero-config hub auto-discovery.** You usually don't even set `NANO_AGENTIC_URL`.
|
|
294
294
|
When nwf runs **embedded**, the engine (`:8080`) serves the console but the
|
|
295
|
-
`/agentic` channel is served by the **embedded app on its own
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
295
|
+
`/agentic` channel is served by the **embedded app on its own port** (e.g.
|
|
296
|
+
`:3000`); the engine's console proxy deliberately refuses WebSocket upgrades
|
|
297
|
+
(nanobpmn ADR 0057 §3 → `501`), so the channel is unreachable via the engine URL.
|
|
298
|
+
With **no** agentic target configured, `work` therefore **auto-discovers** it: it
|
|
299
|
+
reads `GET <engine>/console/api/projects` and, for each running app that
|
|
300
|
+
advertises an agentic UI port (`appUi.enabled === true` and `appUi.port`), probes
|
|
301
|
+
that app's direct `ws://<engine-host>:<port>/agentic`. Discovery runs **against
|
|
302
|
+
the engine's own host** — a local engine keeps probing `127.0.0.1`, while a
|
|
303
|
+
remote/LAN engine (e.g. `merlin.local:8080`) steers the probe back at *itself*
|
|
304
|
+
(`merlin.local:<port>`), never at the worker's own loopback services. It is
|
|
305
|
+
**time-bounded** (≤2s) and never meaningfully delays job polling. The discovered
|
|
306
|
+
host and port are printed for debugging. (An IPv6 literal engine host is bracketed
|
|
307
|
+
in the URL authority, e.g. `ws://[2001:db8::1]:3000/agentic`.)
|
|
304
308
|
|
|
305
309
|
- **Exactly one app →** the worker connects directly to
|
|
306
|
-
`ws
|
|
307
|
-
and appears live with **zero configuration
|
|
310
|
+
`ws://<engine-host>:<appUi.port>/agentic` (bypassing the WS-incapable console
|
|
311
|
+
proxy) and appears live with **zero configuration** — including cross-machine on
|
|
312
|
+
a trusted LAN.
|
|
308
313
|
- **Two or more apps →** the worker **does not guess**: it prints an `ambiguous`
|
|
309
314
|
error naming each discovered `project → :port` and **stops**. Pin the one you
|
|
310
|
-
want and re-run: `export NANO_AGENTIC_URL=http
|
|
311
|
-
`agenticUrl`).
|
|
315
|
+
want and re-run: `export NANO_AGENTIC_URL=http://<engine-host>:<port>` (or
|
|
316
|
+
persist `agenticUrl`).
|
|
312
317
|
- **Nothing discoverable (e.g. pointed at Camunda, or an API-only gateway) →** the
|
|
313
318
|
worker prints a one-line advisory naming `NANO_AGENTIC_URL` and **continues
|
|
314
319
|
doing real work** with the channel simply absent — discovery never fails the
|
package/c8ctl-plugin.js
CHANGED
|
@@ -4170,6 +4170,21 @@ function isLoopbackHost(hostname) {
|
|
|
4170
4170
|
return /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
4171
4171
|
}
|
|
4172
4172
|
|
|
4173
|
+
/**
|
|
4174
|
+
* Format a hostname for the authority component of a `ws://`/`http://` URL:
|
|
4175
|
+
* a bare IPv6 literal (contains `:`, not already bracketed) is wrapped in `[…]`,
|
|
4176
|
+
* everything else is used verbatim. Idempotent — an already-bracketed host is
|
|
4177
|
+
* left as-is. Guards against building an invalid `ws://::1:3000/…` when a raw or
|
|
4178
|
+
* normalized IPv6 host (e.g. the `::1` constant) has not been bracketed.
|
|
4179
|
+
*
|
|
4180
|
+
* @param {string} host a hostname from `URL.hostname` or a normalized loopback
|
|
4181
|
+
* @returns {string}
|
|
4182
|
+
*/
|
|
4183
|
+
function wsHostPart(host) {
|
|
4184
|
+
const h = String(host || '');
|
|
4185
|
+
return h.includes(':') && !h.startsWith('[') ? `[${h}]` : h;
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4173
4188
|
/**
|
|
4174
4189
|
* Normalise the engine's `GET /console/api/projects` payload into the running
|
|
4175
4190
|
* embedded apps that advertise an agentic UI port. Accepts the shapes the
|
|
@@ -4207,24 +4222,26 @@ function normalizeProjectApps(projects) {
|
|
|
4207
4222
|
}
|
|
4208
4223
|
|
|
4209
4224
|
/**
|
|
4210
|
-
* Probe whether an embedded app's
|
|
4211
|
-
*
|
|
4225
|
+
* Probe whether an embedded app's `/agentic` endpoint answers a WebSocket
|
|
4226
|
+
* upgrade. Connects to `ws://<host>:<port>/agentic?token=…` (host defaults to
|
|
4227
|
+
* `127.0.0.1`; a bare IPv6 literal is bracketed for the URL authority) and
|
|
4212
4228
|
* resolves `true` only if the socket opens within `timeoutMs`; a refused
|
|
4213
4229
|
* connection, the console proxy's deliberate `501`, a `404`, or a timeout all
|
|
4214
|
-
* resolve `false`.
|
|
4215
|
-
*
|
|
4230
|
+
* resolve `false`. Self-cleaning — the probe socket is closed as soon as the
|
|
4231
|
+
* outcome is known. Never throws.
|
|
4216
4232
|
*
|
|
4217
|
-
* @param {number} port the app's direct
|
|
4218
|
-
* @param {{ token?: string, WebSocketImpl?: Function, timeoutMs?: number }} [opts]
|
|
4233
|
+
* @param {number} port the app's direct agentic port (`appUi.port`)
|
|
4234
|
+
* @param {{ host?: string, token?: string, WebSocketImpl?: Function, timeoutMs?: number }} [opts]
|
|
4219
4235
|
* @returns {Promise<boolean>}
|
|
4220
4236
|
*/
|
|
4221
4237
|
function probeAgenticChannel(port, {
|
|
4238
|
+
host = '127.0.0.1',
|
|
4222
4239
|
token = LOCAL_AGENTIC_TOKEN,
|
|
4223
4240
|
WebSocketImpl = globalThis.WebSocket,
|
|
4224
4241
|
timeoutMs = AGENTIC_DISCOVERY_TIMEOUT_MS,
|
|
4225
4242
|
} = {}) {
|
|
4226
4243
|
if (typeof WebSocketImpl !== 'function') return Promise.resolve(false);
|
|
4227
|
-
const url = `ws
|
|
4244
|
+
const url = `ws://${wsHostPart(host)}:${port}/agentic?token=${encodeURIComponent(token)}`;
|
|
4228
4245
|
return new Promise((resolve) => {
|
|
4229
4246
|
let done = false;
|
|
4230
4247
|
let ws;
|
|
@@ -4249,19 +4266,21 @@ function probeAgenticChannel(port, {
|
|
|
4249
4266
|
|
|
4250
4267
|
/**
|
|
4251
4268
|
* Auto-discover the embedded nwf agentic hub(s) reachable from an engine base
|
|
4252
|
-
* URL (#75). Reads `GET <engine>/console/api/projects`, keeps the apps that
|
|
4253
|
-
* advertise an agentic UI port, and WS-probes each app's
|
|
4254
|
-
*
|
|
4255
|
-
* WS-incapable console proxy).
|
|
4256
|
-
* loopback
|
|
4257
|
-
*
|
|
4258
|
-
*
|
|
4259
|
-
*
|
|
4260
|
-
*
|
|
4269
|
+
* URL (#75, #96). Reads `GET <engine>/console/api/projects`, keeps the apps that
|
|
4270
|
+
* advertise an agentic UI port, and WS-probes each app's `/agentic` **on the
|
|
4271
|
+
* engine's own host** to confirm the channel is actually served there (bypassing
|
|
4272
|
+
* the WS-incapable console proxy). Works cross-machine on a trusted LAN: a
|
|
4273
|
+
* loopback engine probes `127.0.0.1`, a remote engine (e.g. `merlin.local`)
|
|
4274
|
+
* probes that same host — the port is taken from the projects API but the host is
|
|
4275
|
+
* always the engine's, so a rogue projects API can never steer a probe at the
|
|
4276
|
+
* worker's own loopback (#76). Enforces a single shared time budget across the
|
|
4277
|
+
* fetch + probes, and is fail-open: any error — not a nano engine (Camunda),
|
|
4278
|
+
* network failure, malformed body, or an overall timeout — degrades to `[]` so
|
|
4279
|
+
* the worker's real job is never blocked.
|
|
4261
4280
|
*
|
|
4262
|
-
* @param {string} engineBaseUrl the engine base URL (e.g. `http://
|
|
4281
|
+
* @param {string} engineBaseUrl the engine base URL (e.g. `http://merlin.local:8080`)
|
|
4263
4282
|
* @param {{ token?: string, fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
|
|
4264
|
-
* @returns {Promise<Array<{ project: string, port: number, label?: string }>>}
|
|
4283
|
+
* @returns {Promise<Array<{ project: string, port: number, label?: string, host: string }>>}
|
|
4265
4284
|
*/
|
|
4266
4285
|
async function discoverAgenticHubs(engineBaseUrl, {
|
|
4267
4286
|
token = LOCAL_AGENTIC_TOKEN,
|
|
@@ -4273,16 +4292,21 @@ async function discoverAgenticHubs(engineBaseUrl, {
|
|
|
4273
4292
|
return [];
|
|
4274
4293
|
}
|
|
4275
4294
|
const base = engineBaseUrl.replace(/\/+$/, '');
|
|
4276
|
-
//
|
|
4277
|
-
//
|
|
4278
|
-
//
|
|
4295
|
+
// Discover against the ENGINE's own host — the app is embedded in the engine,
|
|
4296
|
+
// so its /agentic port lives on the same host the worker already trusts as its
|
|
4297
|
+
// engine (that's where it pulls jobs from). A loopback engine keeps probing
|
|
4298
|
+
// 127.0.0.1 (unchanged local behaviour); a remote/LAN engine (e.g.
|
|
4299
|
+
// merlin.local) steers the probe back to ITSELF, never at the worker's own
|
|
4300
|
+
// loopback services — which was the actual #76 concern (a rogue projects API
|
|
4301
|
+
// making the worker probe its own localhost). So the port comes from the
|
|
4302
|
+
// engine's projects API, but the HOST is always the engine's, never guessed.
|
|
4279
4303
|
let host;
|
|
4280
4304
|
try {
|
|
4281
4305
|
host = new URL(base).hostname;
|
|
4282
4306
|
} catch {
|
|
4283
4307
|
return [];
|
|
4284
4308
|
}
|
|
4285
|
-
|
|
4309
|
+
const probeHost = isLoopbackHost(host) ? '127.0.0.1' : host;
|
|
4286
4310
|
// Single discovery budget: the projects fetch and the WS probes share ONE
|
|
4287
4311
|
// deadline, so total discovery can't approach 2× timeoutMs (the fetch could
|
|
4288
4312
|
// consume ~timeoutMs and then each probe was previously given a fresh full
|
|
@@ -4304,10 +4328,13 @@ async function discoverAgenticHubs(engineBaseUrl, {
|
|
|
4304
4328
|
if (apps.length === 0) return [];
|
|
4305
4329
|
const remainingMs = deadline - Date.now();
|
|
4306
4330
|
if (remainingMs <= 0) return [];
|
|
4307
|
-
// Probe candidate ports concurrently within the remaining shared budget.
|
|
4331
|
+
// Probe candidate ports concurrently within the remaining shared budget. Each
|
|
4332
|
+
// surviving hub carries the engine host so the caller builds the right URL.
|
|
4308
4333
|
const settled = await Promise.all(apps.map(async (app) => {
|
|
4309
4334
|
try {
|
|
4310
|
-
return (await wsProbe(app.port, { token, timeoutMs: remainingMs }))
|
|
4335
|
+
return (await wsProbe(app.port, { host: probeHost, token, timeoutMs: remainingMs }))
|
|
4336
|
+
? { ...app, host: probeHost }
|
|
4337
|
+
: null;
|
|
4311
4338
|
} catch {
|
|
4312
4339
|
return null;
|
|
4313
4340
|
}
|
|
@@ -4324,7 +4351,8 @@ async function discoverAgenticHubs(engineBaseUrl, {
|
|
|
4324
4351
|
* half-configured. No discovery attempted.
|
|
4325
4352
|
* - `{ status: 'connect', config }` — a target to connect to. Either the
|
|
4326
4353
|
* explicit `NANO_AGENTIC_URL`/`agenticUrl` verbatim (no discovery), or the
|
|
4327
|
-
* single discovered app's
|
|
4354
|
+
* single discovered app's `ws://<engineHost>:<port>/agentic` (loopback for a
|
|
4355
|
+
* local engine, the engine's LAN host for a remote one).
|
|
4328
4356
|
* - `{ status: 'ambiguous', message, candidates }` — two+ apps expose a
|
|
4329
4357
|
* channel. Hard stop for the worker: it must not silently pick one.
|
|
4330
4358
|
* - `{ status: 'advisory', message }` — nothing discoverable (zero matches,
|
|
@@ -4342,11 +4370,24 @@ async function resolveAgenticTarget(opts = {}) {
|
|
|
4342
4370
|
|
|
4343
4371
|
const hubs = await discoverAgenticHubs(base.url, { token: base.token, ...opts });
|
|
4344
4372
|
|
|
4373
|
+
// The host to suggest in operator-facing messages: the engine's own host
|
|
4374
|
+
// (bracketed if an IPv6 literal, so the suggested URL authority is valid), so a
|
|
4375
|
+
// remote-engine advisory names the reachable LAN host rather than 127.0.0.1.
|
|
4376
|
+
let suggestHost = '127.0.0.1';
|
|
4377
|
+
try {
|
|
4378
|
+
const h = new URL(base.url).hostname;
|
|
4379
|
+
suggestHost = wsHostPart(isLoopbackHost(h) ? '127.0.0.1' : h);
|
|
4380
|
+
} catch { /* keep the loopback default */ }
|
|
4381
|
+
|
|
4345
4382
|
if (hubs.length === 1) {
|
|
4346
|
-
const { project, port } = hubs[0];
|
|
4383
|
+
const { project, port, host } = hubs[0];
|
|
4347
4384
|
return {
|
|
4348
4385
|
status: 'connect',
|
|
4349
|
-
config: {
|
|
4386
|
+
config: {
|
|
4387
|
+
...base,
|
|
4388
|
+
url: `http://${wsHostPart(host)}:${port}`,
|
|
4389
|
+
discovered: { project, port, host },
|
|
4390
|
+
},
|
|
4350
4391
|
};
|
|
4351
4392
|
}
|
|
4352
4393
|
if (hubs.length > 1) {
|
|
@@ -4355,14 +4396,14 @@ async function resolveAgenticTarget(opts = {}) {
|
|
|
4355
4396
|
status: 'ambiguous',
|
|
4356
4397
|
candidates: hubs,
|
|
4357
4398
|
message: `multiple embedded apps expose an agentic channel (${list}); refusing to guess. `
|
|
4358
|
-
+
|
|
4399
|
+
+ `Disambiguate by setting NANO_AGENTIC_URL=http://${suggestHost}:<port> (or persisted agenticUrl) to the one you want.`,
|
|
4359
4400
|
};
|
|
4360
4401
|
}
|
|
4361
4402
|
return {
|
|
4362
4403
|
status: 'advisory',
|
|
4363
4404
|
message: `agentic visibility was not discoverable at ${base.url} — the embedded app port could `
|
|
4364
4405
|
+ 'not be found (not a nano engine, or its console projects API is absent). Set '
|
|
4365
|
-
+
|
|
4406
|
+
+ `NANO_AGENTIC_URL=http://${suggestHost}:<appUi.port> to enable the visibility channel. Continuing without it.`,
|
|
4366
4407
|
};
|
|
4367
4408
|
}
|
|
4368
4409
|
|
|
@@ -4744,7 +4785,8 @@ async function workAgent(req, flags) {
|
|
|
4744
4785
|
const shown = redactAgenticUrl(buildAgenticUrl(agenticCfg.url, {}));
|
|
4745
4786
|
const mode = agenticCfg.secure ? 'secure' : 'local';
|
|
4746
4787
|
if (agenticCfg.discovered) {
|
|
4747
|
-
|
|
4788
|
+
const d = agenticCfg.discovered;
|
|
4789
|
+
logger.info(` agentic channel: auto-discovered ${d.project} on the app's /agentic port ${wsHostPart(d.host)}:${d.port} (bypassing the WS-incapable console proxy).`);
|
|
4748
4790
|
}
|
|
4749
4791
|
logger.info(` agentic channel (${mode}): announcing presence as ${workerName} on ${shown}`);
|
|
4750
4792
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.2",
|
|
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.36.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.36.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.36.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.36.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.36.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.36.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.36.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.36.2",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.36.2",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.36.2",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.36.2",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.36.2",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.36.2",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.36.2"
|
|
67
67
|
}
|
|
68
68
|
}
|