c8ctl-plugin-nano 1.34.0 → 1.35.1
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 +29 -0
- package/c8ctl-plugin.js +309 -10
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -289,6 +289,35 @@ c8ctl nano work reviewer
|
|
|
289
289
|
# agentic channel (local): announcing presence as ‹worker› on ws://localhost:8080/agentic
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
+
**Zero-config hub auto-discovery.** You usually don't even set `NANO_AGENTIC_URL`.
|
|
293
|
+
When nwf runs **embedded**, the engine (`:8080`) serves the console but the
|
|
294
|
+
`/agentic` channel is served by the **embedded app on its own loopback port**
|
|
295
|
+
(e.g. `:3000`); the engine's console proxy deliberately refuses WebSocket
|
|
296
|
+
upgrades (nanobpmn ADR 0057 §3 → `501`), so the channel is unreachable via the
|
|
297
|
+
engine URL. With **no** agentic target configured, `work` therefore
|
|
298
|
+
**auto-discovers** it: it reads `GET <engine>/console/api/projects` and, for each
|
|
299
|
+
running app that advertises an agentic UI port (`appUi.enabled === true` and
|
|
300
|
+
`appUi.port`), probes that app's direct `ws://127.0.0.1:<port>/agentic`. Discovery
|
|
301
|
+
is **loopback-only** and **time-bounded** (≤2s) and never meaningfully delays job
|
|
302
|
+
polling.
|
|
303
|
+
|
|
304
|
+
- **Exactly one app →** the worker connects directly to
|
|
305
|
+
`ws://127.0.0.1:<appUi.port>/agentic` (bypassing the WS-incapable console proxy)
|
|
306
|
+
and appears live with **zero configuration**.
|
|
307
|
+
- **Two or more apps →** the worker **does not guess**: it prints an `ambiguous`
|
|
308
|
+
error naming each discovered `project → :port` and **stops**. Pin the one you
|
|
309
|
+
want and re-run: `export NANO_AGENTIC_URL=http://127.0.0.1:<port>` (or persist
|
|
310
|
+
`agenticUrl`).
|
|
311
|
+
- **Nothing discoverable (e.g. pointed at Camunda, or an API-only gateway) →** the
|
|
312
|
+
worker prints a one-line advisory naming `NANO_AGENTIC_URL` and **continues
|
|
313
|
+
doing real work** with the channel simply absent — discovery never fails the
|
|
314
|
+
worker's actual job.
|
|
315
|
+
|
|
316
|
+
Setting `NANO_AGENTIC_URL` (or persisted `agenticUrl`) **skips discovery** and is
|
|
317
|
+
used **verbatim** — so an explicit target always wins, and it's also how you
|
|
318
|
+
disambiguate when several apps are running. `NANO_AGENTIC=off` disables the
|
|
319
|
+
channel entirely and attempts **no** discovery.
|
|
320
|
+
|
|
292
321
|
**Secure mode (opt-in).** For a shared/remote deployment, enrol the worker with an
|
|
293
322
|
ADR 0028 **identity token** and a **capability credential** (the same `?token=…`
|
|
294
323
|
pattern the blackboard uses). Setting either switches the worker into SECURE mode,
|
package/c8ctl-plugin.js
CHANGED
|
@@ -2549,14 +2549,18 @@ function resourceContentUrl(baseUrl, resourceKey) {
|
|
|
2549
2549
|
// is surfaced as a ProvisionError so the caller fails the job with a clear
|
|
2550
2550
|
// provisioning message (never runs an agent with a silently-empty prompt).
|
|
2551
2551
|
async function fetchLinkedResourceContent(resourceKey, opts = {}) {
|
|
2552
|
-
const { baseUrl, token, fetchImpl = fetch, timeoutMs = 15_000 } = opts;
|
|
2552
|
+
const { baseUrl, token, authHeaders, fetchImpl = fetch, timeoutMs = 15_000 } = opts;
|
|
2553
2553
|
const url = resourceContentUrl(baseUrl, resourceKey);
|
|
2554
2554
|
const controller = new AbortController();
|
|
2555
2555
|
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
2556
2556
|
let res;
|
|
2557
2557
|
try {
|
|
2558
2558
|
const headers = { Accept: 'application/octet-stream' };
|
|
2559
|
-
|
|
2559
|
+
// A ready-made auth-headers map (from the activating SDK client's
|
|
2560
|
+
// getAuthHeaders(), covering OAuth/basic/none) wins over a bare token. An
|
|
2561
|
+
// empty map means unauthenticated — deliberately no Authorization header.
|
|
2562
|
+
if (authHeaders && typeof authHeaders === 'object') Object.assign(headers, authHeaders);
|
|
2563
|
+
else if (token) headers.Authorization = `Bearer ${token}`;
|
|
2560
2564
|
res = await fetchImpl(url, { method: 'GET', headers, signal: controller.signal });
|
|
2561
2565
|
} catch (err) {
|
|
2562
2566
|
throw new ProvisionError(`prompt resource ${resourceKey} fetch failed: ${err && err.message ? err.message : String(err)}`);
|
|
@@ -2581,14 +2585,57 @@ async function fetchLinkedResourceContent(resourceKey, opts = {}) {
|
|
|
2581
2585
|
// prompt resource cannot be fetched — a provisioning failure, not a silent
|
|
2582
2586
|
// empty-prompt run.
|
|
2583
2587
|
async function resolveLinkedPrompt(customHeaders, opts = {}) {
|
|
2584
|
-
const { linkName = DEFAULT_PROMPT_LINK_NAME, baseUrl, token, fetchImpl, timeoutMs } = opts;
|
|
2588
|
+
const { linkName = DEFAULT_PROMPT_LINK_NAME, baseUrl, token, authHeaders, fetchImpl, timeoutMs } = opts;
|
|
2585
2589
|
const entry = pickLinkedResource(parseLinkedResources(customHeaders), linkName);
|
|
2586
2590
|
if (!entry) return null;
|
|
2587
2591
|
const resourceKey = entry.resourceKey;
|
|
2588
|
-
const basePrompt = await fetchLinkedResourceContent(resourceKey, { baseUrl, token, fetchImpl, timeoutMs });
|
|
2592
|
+
const basePrompt = await fetchLinkedResourceContent(resourceKey, { baseUrl, token, authHeaders, fetchImpl, timeoutMs });
|
|
2589
2593
|
return { basePrompt, resourceKey, resourceType: entry.resourceType ?? null, linkName: String(linkName) };
|
|
2590
2594
|
}
|
|
2591
2595
|
|
|
2596
|
+
// Normalize a client-configured REST address for use as a linked-resource fetch
|
|
2597
|
+
// base. The SDK's restAddress may or may not already include the `/v2` API
|
|
2598
|
+
// prefix (CAMUNDA_REST_ADDRESS accepts either); resourceContentUrl re-adds it,
|
|
2599
|
+
// so strip a trailing `/v2` (and any trailing slashes) to avoid a double `/v2`.
|
|
2600
|
+
function normalizeRestBase(addr) {
|
|
2601
|
+
return String(addr || '').replace(/\/+$/, '').replace(/\/v2$/i, '');
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// Derive the linked-resource fetch base URL + auth headers from the SAME SDK
|
|
2605
|
+
// client that activated the job. A linked-resource `resourceKey` is broker-local,
|
|
2606
|
+
// so prompt content must be fetched from the broker this worker is connected to
|
|
2607
|
+
// — never a localhost default (the cause of "prompt resource N fetch failed").
|
|
2608
|
+
// An explicit NANO_REST_URL / NANO_REST_TOKEN override still wins as an operator
|
|
2609
|
+
// escape hatch. Both getConfig()/getAuthHeaders() are guarded so an older or
|
|
2610
|
+
// atypical client runtime degrades to the override/legacy path rather than throw.
|
|
2611
|
+
//
|
|
2612
|
+
// TODO: once c8ctl bumps @camunda8/orchestration-cluster-api to v10 (10.0.0-alpha
|
|
2613
|
+
// exposes the typed camunda.getResourceContentBinary({resourceKey}) → Blob), drop
|
|
2614
|
+
// this raw /content/binary fetch and call that method directly. The pinned ^9.1.0
|
|
2615
|
+
// SDK only exposes the deprecated getResourceContent, which 406s for generic
|
|
2616
|
+
// (Markdown) prompt resources — see camunda/orchestration-cluster-api-js.
|
|
2617
|
+
async function resolveLinkedPromptSource(camunda, env = process.env) {
|
|
2618
|
+
let baseUrl = env.NANO_REST_URL || '';
|
|
2619
|
+
if (!baseUrl && camunda && typeof camunda.getConfig === 'function') {
|
|
2620
|
+
try {
|
|
2621
|
+
baseUrl = normalizeRestBase(camunda.getConfig().restAddress);
|
|
2622
|
+
} catch {
|
|
2623
|
+
// ignore — fall back to the legacy resolveBrokerRestConfig base below
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
let authHeaders;
|
|
2627
|
+
if (env.NANO_REST_TOKEN) {
|
|
2628
|
+
authHeaders = { Authorization: `Bearer ${env.NANO_REST_TOKEN}` };
|
|
2629
|
+
} else if (camunda && typeof camunda.getAuthHeaders === 'function') {
|
|
2630
|
+
try {
|
|
2631
|
+
authHeaders = await camunda.getAuthHeaders();
|
|
2632
|
+
} catch {
|
|
2633
|
+
authHeaders = undefined;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return { baseUrl, authHeaders };
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2592
2639
|
// Secrets are referenced by NAME, never by value, in the model. A resolver maps
|
|
2593
2640
|
// a name → value at run time. The wrapper injects them into the child ENV, so
|
|
2594
2641
|
// values never appear in argv or `docker inspect`.
|
|
@@ -3834,6 +3881,11 @@ function resolveAgenticConfig() {
|
|
|
3834
3881
|
?? (cfg.agentic === false ? 'off' : cfg.agentic);
|
|
3835
3882
|
if (/^(0|off|false|no)$/i.test(String(offSetting ?? ''))) return null;
|
|
3836
3883
|
|
|
3884
|
+
// An explicit agentic target (env NANO_AGENTIC_URL or persisted `agenticUrl`)
|
|
3885
|
+
// is used verbatim and short-circuits hub auto-discovery (#75). When neither is
|
|
3886
|
+
// set the URL below is the ENGINE base (nanoUrl → NANO_BASE_URL → default), off
|
|
3887
|
+
// which `resolveAgenticTarget()` discovers the embedded app's own /agentic port.
|
|
3888
|
+
const explicitUrl = !!(process.env.NANO_AGENTIC_URL || cfg.agenticUrl);
|
|
3837
3889
|
const url = process.env.NANO_AGENTIC_URL
|
|
3838
3890
|
|| cfg.agenticUrl
|
|
3839
3891
|
|| cfg.nanoUrl
|
|
@@ -3853,11 +3905,230 @@ function resolveAgenticConfig() {
|
|
|
3853
3905
|
// enrolment — require BOTH halves, fail closed if only one is present.
|
|
3854
3906
|
if (token || credential) {
|
|
3855
3907
|
if (!token || !credential) return null;
|
|
3856
|
-
return { url, token, credential, bufferCapacity, secure: true };
|
|
3908
|
+
return { url, token, credential, bufferCapacity, secure: true, explicitUrl };
|
|
3857
3909
|
}
|
|
3858
3910
|
|
|
3859
3911
|
// LOCAL mode (default): well-known localhost token, no capability credential.
|
|
3860
|
-
return { url, token: LOCAL_AGENTIC_TOKEN, credential: '', bufferCapacity, secure: false };
|
|
3912
|
+
return { url, token: LOCAL_AGENTIC_TOKEN, credential: '', bufferCapacity, secure: false, explicitUrl };
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
// Total budget for zero-config hub auto-discovery (#75): the projects-API read
|
|
3916
|
+
// and every WS upgrade probe must degrade to a "not discoverable" advisory
|
|
3917
|
+
// within this window so discovery never meaningfully delays job polling.
|
|
3918
|
+
const AGENTIC_DISCOVERY_TIMEOUT_MS = 2_000;
|
|
3919
|
+
|
|
3920
|
+
/**
|
|
3921
|
+
* Is `hostname` a loopback address? Discovery reads the engine's projects API
|
|
3922
|
+
* and then probes `127.0.0.1:<advertised port>`, so it must only run against a
|
|
3923
|
+
* loopback engine — otherwise a remote engine's response would steer a local
|
|
3924
|
+
* port probe, breaking the loopback-only guarantee (#75/#76). Accepts
|
|
3925
|
+
* `localhost`, the IPv4 loopback block `127.0.0.0/8`, and IPv6 `::1` (with or
|
|
3926
|
+
* without URL brackets); case-insensitive and trimmed.
|
|
3927
|
+
*
|
|
3928
|
+
* @param {string} hostname a URL hostname (e.g. `127.0.0.1`, `localhost`, `[::1]`)
|
|
3929
|
+
* @returns {boolean}
|
|
3930
|
+
*/
|
|
3931
|
+
function isLoopbackHost(hostname) {
|
|
3932
|
+
const h = String(hostname || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
3933
|
+
if (!h) return false;
|
|
3934
|
+
if (h === 'localhost' || h === '::1') return true;
|
|
3935
|
+
return /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
/**
|
|
3939
|
+
* Normalise the engine's `GET /console/api/projects` payload into the running
|
|
3940
|
+
* embedded apps that advertise an agentic UI port. Accepts the shapes the
|
|
3941
|
+
* console may serve — a keyed map (`{ "Nano_Workforce": { appUi } }`), a bare
|
|
3942
|
+
* array (`[{ name, appUi }]`), or a wrapped array (`{ projects: [...] }`) — and
|
|
3943
|
+
* keeps only apps with `appUi.enabled === true` and a positive integer
|
|
3944
|
+
* `appUi.port`. Anything else (a Camunda engine, an API-only gateway, malformed
|
|
3945
|
+
* JSON) yields `[]`.
|
|
3946
|
+
*
|
|
3947
|
+
* @param {unknown} projects the parsed projects-API body
|
|
3948
|
+
* @returns {Array<{ project: string, port: number, label?: string }>}
|
|
3949
|
+
*/
|
|
3950
|
+
function normalizeProjectApps(projects) {
|
|
3951
|
+
if (!projects || typeof projects !== 'object') return [];
|
|
3952
|
+
let entries;
|
|
3953
|
+
if (Array.isArray(projects)) {
|
|
3954
|
+
entries = projects.map((p) => [p?.name ?? p?.project ?? p?.id, p]);
|
|
3955
|
+
} else if (Array.isArray(projects.projects)) {
|
|
3956
|
+
entries = projects.projects.map((p) => [p?.name ?? p?.project ?? p?.id, p]);
|
|
3957
|
+
} else {
|
|
3958
|
+
entries = Object.entries(projects);
|
|
3959
|
+
}
|
|
3960
|
+
const apps = [];
|
|
3961
|
+
for (const [name, proj] of entries) {
|
|
3962
|
+
const ui = proj?.appUi;
|
|
3963
|
+
if (ui && ui.enabled === true && Number.isInteger(ui.port) && ui.port > 0) {
|
|
3964
|
+
apps.push({
|
|
3965
|
+
project: String(name ?? ui.label ?? ui.port),
|
|
3966
|
+
port: ui.port,
|
|
3967
|
+
label: ui.label,
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
return apps;
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
/**
|
|
3975
|
+
* Probe whether an embedded app's own loopback `/agentic` endpoint answers a
|
|
3976
|
+
* WebSocket upgrade. Connects to `ws://127.0.0.1:<port>/agentic?token=…` and
|
|
3977
|
+
* resolves `true` only if the socket opens within `timeoutMs`; a refused
|
|
3978
|
+
* connection, the console proxy's deliberate `501`, a `404`, or a timeout all
|
|
3979
|
+
* resolve `false`. Loopback-only and self-cleaning — the probe socket is closed
|
|
3980
|
+
* as soon as the outcome is known. Never throws.
|
|
3981
|
+
*
|
|
3982
|
+
* @param {number} port the app's direct loopback port (`appUi.port`)
|
|
3983
|
+
* @param {{ token?: string, WebSocketImpl?: Function, timeoutMs?: number }} [opts]
|
|
3984
|
+
* @returns {Promise<boolean>}
|
|
3985
|
+
*/
|
|
3986
|
+
function probeAgenticChannel(port, {
|
|
3987
|
+
token = LOCAL_AGENTIC_TOKEN,
|
|
3988
|
+
WebSocketImpl = globalThis.WebSocket,
|
|
3989
|
+
timeoutMs = AGENTIC_DISCOVERY_TIMEOUT_MS,
|
|
3990
|
+
} = {}) {
|
|
3991
|
+
if (typeof WebSocketImpl !== 'function') return Promise.resolve(false);
|
|
3992
|
+
const url = `ws://127.0.0.1:${port}/agentic?token=${encodeURIComponent(token)}`;
|
|
3993
|
+
return new Promise((resolve) => {
|
|
3994
|
+
let done = false;
|
|
3995
|
+
let ws;
|
|
3996
|
+
const finish = (ok) => {
|
|
3997
|
+
if (done) return;
|
|
3998
|
+
done = true;
|
|
3999
|
+
clearTimeout(timer);
|
|
4000
|
+
try { ws?.close(); } catch { /* best-effort cleanup */ }
|
|
4001
|
+
resolve(ok);
|
|
4002
|
+
};
|
|
4003
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
4004
|
+
try {
|
|
4005
|
+
ws = new WebSocketImpl(url);
|
|
4006
|
+
ws.onopen = () => finish(true);
|
|
4007
|
+
ws.onerror = () => finish(false);
|
|
4008
|
+
ws.onclose = () => finish(false);
|
|
4009
|
+
} catch {
|
|
4010
|
+
finish(false);
|
|
4011
|
+
}
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
/**
|
|
4016
|
+
* Auto-discover the embedded nwf agentic hub(s) reachable from an engine base
|
|
4017
|
+
* URL (#75). Reads `GET <engine>/console/api/projects`, keeps the apps that
|
|
4018
|
+
* advertise an agentic UI port, and WS-probes each app's direct loopback
|
|
4019
|
+
* `/agentic` to confirm the channel is actually served there (bypassing the
|
|
4020
|
+
* WS-incapable console proxy). Loopback-only (the engine host itself must be
|
|
4021
|
+
* loopback, since the response steers a local port probe), enforces a single
|
|
4022
|
+
* shared time budget across the fetch + probes, and is fail-open: any error —
|
|
4023
|
+
* not a nano engine (Camunda), a non-loopback engine, network failure,
|
|
4024
|
+
* malformed body, or an overall timeout — degrades to `[]` so the worker's real
|
|
4025
|
+
* job is never blocked.
|
|
4026
|
+
*
|
|
4027
|
+
* @param {string} engineBaseUrl the engine base URL (e.g. `http://localhost:8080`)
|
|
4028
|
+
* @param {{ token?: string, fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
|
|
4029
|
+
* @returns {Promise<Array<{ project: string, port: number, label?: string }>>}
|
|
4030
|
+
*/
|
|
4031
|
+
async function discoverAgenticHubs(engineBaseUrl, {
|
|
4032
|
+
token = LOCAL_AGENTIC_TOKEN,
|
|
4033
|
+
fetchImpl = globalThis.fetch,
|
|
4034
|
+
wsProbe = probeAgenticChannel,
|
|
4035
|
+
timeoutMs = AGENTIC_DISCOVERY_TIMEOUT_MS,
|
|
4036
|
+
} = {}) {
|
|
4037
|
+
if (typeof fetchImpl !== 'function' || typeof engineBaseUrl !== 'string' || !engineBaseUrl.trim()) {
|
|
4038
|
+
return [];
|
|
4039
|
+
}
|
|
4040
|
+
const base = engineBaseUrl.replace(/\/+$/, '');
|
|
4041
|
+
// Loopback-only: discovery probes 127.0.0.1:<port> using a port advertised by
|
|
4042
|
+
// the engine's projects API, so a non-loopback (remote) engine could steer a
|
|
4043
|
+
// local port probe. Refuse discovery unless the engine host is loopback (#76).
|
|
4044
|
+
let host;
|
|
4045
|
+
try {
|
|
4046
|
+
host = new URL(base).hostname;
|
|
4047
|
+
} catch {
|
|
4048
|
+
return [];
|
|
4049
|
+
}
|
|
4050
|
+
if (!isLoopbackHost(host)) return [];
|
|
4051
|
+
// Single discovery budget: the projects fetch and the WS probes share ONE
|
|
4052
|
+
// deadline, so total discovery can't approach 2× timeoutMs (the fetch could
|
|
4053
|
+
// consume ~timeoutMs and then each probe was previously given a fresh full
|
|
4054
|
+
// budget). Probes get only the time left after the fetch (#76).
|
|
4055
|
+
const deadline = Date.now() + timeoutMs;
|
|
4056
|
+
let projects;
|
|
4057
|
+
const controller = new AbortController();
|
|
4058
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4059
|
+
try {
|
|
4060
|
+
const res = await fetchImpl(`${base}/console/api/projects`, { signal: controller.signal });
|
|
4061
|
+
if (!res || !res.ok) return [];
|
|
4062
|
+
projects = await res.json();
|
|
4063
|
+
} catch {
|
|
4064
|
+
return [];
|
|
4065
|
+
} finally {
|
|
4066
|
+
clearTimeout(timer);
|
|
4067
|
+
}
|
|
4068
|
+
const apps = normalizeProjectApps(projects);
|
|
4069
|
+
if (apps.length === 0) return [];
|
|
4070
|
+
const remainingMs = deadline - Date.now();
|
|
4071
|
+
if (remainingMs <= 0) return [];
|
|
4072
|
+
// Probe candidate ports concurrently within the remaining shared budget.
|
|
4073
|
+
const settled = await Promise.all(apps.map(async (app) => {
|
|
4074
|
+
try {
|
|
4075
|
+
return (await wsProbe(app.port, { token, timeoutMs: remainingMs })) ? app : null;
|
|
4076
|
+
} catch {
|
|
4077
|
+
return null;
|
|
4078
|
+
}
|
|
4079
|
+
}));
|
|
4080
|
+
return settled.filter(Boolean);
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
/**
|
|
4084
|
+
* Resolve the final agentic-channel target for a worker, running zero-config hub
|
|
4085
|
+
* auto-discovery when no explicit target is configured (#75). Layered on top of
|
|
4086
|
+
* {@link resolveAgenticConfig}:
|
|
4087
|
+
*
|
|
4088
|
+
* - `{ status: 'off' }` — visibility disabled (off-switch) or SECURE mode
|
|
4089
|
+
* half-configured. No discovery attempted.
|
|
4090
|
+
* - `{ status: 'connect', config }` — a target to connect to. Either the
|
|
4091
|
+
* explicit `NANO_AGENTIC_URL`/`agenticUrl` verbatim (no discovery), or the
|
|
4092
|
+
* single discovered app's direct `ws://127.0.0.1:<port>/agentic` loopback.
|
|
4093
|
+
* - `{ status: 'ambiguous', message, candidates }` — two+ apps expose a
|
|
4094
|
+
* channel. Hard stop for the worker: it must not silently pick one.
|
|
4095
|
+
* - `{ status: 'advisory', message }` — nothing discoverable (zero matches,
|
|
4096
|
+
* no projects API / not a nano engine, or discovery error/timeout). The
|
|
4097
|
+
* worker continues doing real work with no channel.
|
|
4098
|
+
*
|
|
4099
|
+
* @param {{ fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
|
|
4100
|
+
* @returns {Promise<{ status: string, config?: object, message?: string, candidates?: Array }>}
|
|
4101
|
+
*/
|
|
4102
|
+
async function resolveAgenticTarget(opts = {}) {
|
|
4103
|
+
const base = resolveAgenticConfig();
|
|
4104
|
+
if (!base) return { status: 'off' };
|
|
4105
|
+
// Explicit target wins verbatim and skips discovery entirely.
|
|
4106
|
+
if (base.explicitUrl) return { status: 'connect', config: base };
|
|
4107
|
+
|
|
4108
|
+
const hubs = await discoverAgenticHubs(base.url, { token: base.token, ...opts });
|
|
4109
|
+
|
|
4110
|
+
if (hubs.length === 1) {
|
|
4111
|
+
const { project, port } = hubs[0];
|
|
4112
|
+
return {
|
|
4113
|
+
status: 'connect',
|
|
4114
|
+
config: { ...base, url: `http://127.0.0.1:${port}`, discovered: { project, port } },
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
if (hubs.length > 1) {
|
|
4118
|
+
const list = hubs.map((h) => `${h.project} → :${h.port}`).join(', ');
|
|
4119
|
+
return {
|
|
4120
|
+
status: 'ambiguous',
|
|
4121
|
+
candidates: hubs,
|
|
4122
|
+
message: `multiple embedded apps expose an agentic channel (${list}); refusing to guess. `
|
|
4123
|
+
+ 'Disambiguate by setting NANO_AGENTIC_URL=http://127.0.0.1:<port> (or persisted agenticUrl) to the one you want.',
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
return {
|
|
4127
|
+
status: 'advisory',
|
|
4128
|
+
message: `agentic visibility was not discoverable at ${base.url} — the embedded app port could `
|
|
4129
|
+
+ 'not be found (not a nano engine, or its console projects API is absent). Set '
|
|
4130
|
+
+ 'NANO_AGENTIC_URL=http://127.0.0.1:<appUi.port> to enable the visibility channel. Continuing without it.',
|
|
4131
|
+
};
|
|
3861
4132
|
}
|
|
3862
4133
|
|
|
3863
4134
|
/**
|
|
@@ -4184,7 +4455,26 @@ async function workAgent(req, flags) {
|
|
|
4184
4455
|
// worker joins with the well-known localhost token and no credential; SECURE
|
|
4185
4456
|
// mode (NANO_AGENTIC_TOKEN + NANO_AGENTIC_CREDENTIAL) sends a real ADR 0028
|
|
4186
4457
|
// identity + capability; NANO_AGENTIC=off disables it (see resolveAgenticConfig).
|
|
4187
|
-
const
|
|
4458
|
+
const agenticTarget = await resolveAgenticTarget({ logger });
|
|
4459
|
+
let agenticCfg = null;
|
|
4460
|
+
switch (agenticTarget.status) {
|
|
4461
|
+
case 'connect':
|
|
4462
|
+
agenticCfg = agenticTarget.config;
|
|
4463
|
+
break;
|
|
4464
|
+
case 'ambiguous':
|
|
4465
|
+
// The operator ran with visibility on-by-default but the hub is
|
|
4466
|
+
// unresolved — a misconfiguration to fix, not to guess through. Hard stop.
|
|
4467
|
+
logger.error(` agentic visibility is ambiguous: ${agenticTarget.message}`);
|
|
4468
|
+
process.exit(1);
|
|
4469
|
+
break;
|
|
4470
|
+
case 'advisory':
|
|
4471
|
+
logger.info(` agentic channel: ${agenticTarget.message}`);
|
|
4472
|
+
break;
|
|
4473
|
+
case 'off':
|
|
4474
|
+
default:
|
|
4475
|
+
logger.info(' agentic channel: disabled — either the off-switch is set (NANO_AGENTIC=off or persisted agentic:false), or SECURE mode is half-configured (set BOTH NANO_AGENTIC_TOKEN + NANO_AGENTIC_CREDENTIAL). Clear the off-switch to use default LOCAL visibility.');
|
|
4476
|
+
break;
|
|
4477
|
+
}
|
|
4188
4478
|
if (agenticCfg) {
|
|
4189
4479
|
try {
|
|
4190
4480
|
workChannel = await createWorkChannel({
|
|
@@ -4204,6 +4494,9 @@ async function workAgent(req, flags) {
|
|
|
4204
4494
|
});
|
|
4205
4495
|
const shown = redactAgenticUrl(buildAgenticUrl(agenticCfg.url, {}));
|
|
4206
4496
|
const mode = agenticCfg.secure ? 'secure' : 'local';
|
|
4497
|
+
if (agenticCfg.discovered) {
|
|
4498
|
+
logger.info(` agentic channel: auto-discovered ${agenticCfg.discovered.project} on the embedded app port :${agenticCfg.discovered.port} (bypassing the WS-incapable console proxy).`);
|
|
4499
|
+
}
|
|
4207
4500
|
logger.info(` agentic channel (${mode}): announcing presence as ${workerName} on ${shown}`);
|
|
4208
4501
|
} catch (err) {
|
|
4209
4502
|
// Never let a channel failure stop the worker from doing its actual job.
|
|
@@ -4227,8 +4520,6 @@ async function workAgent(req, flags) {
|
|
|
4227
4520
|
logger.warn(` agentic buffer monitor unavailable (${err?.message || err}); channel presence still active.`);
|
|
4228
4521
|
}
|
|
4229
4522
|
}
|
|
4230
|
-
} else {
|
|
4231
|
-
logger.info(' agentic channel: disabled — either the off-switch is set (NANO_AGENTIC=off or persisted agentic:false), or SECURE mode is half-configured (set BOTH NANO_AGENTIC_TOKEN + NANO_AGENTIC_CREDENTIAL). Clear the off-switch to use default LOCAL visibility.');
|
|
4232
4523
|
}
|
|
4233
4524
|
|
|
4234
4525
|
// C3 (#42): the role's live-terminal mode — a full PTY (streamed on the relay
|
|
@@ -4289,8 +4580,13 @@ async function workAgent(req, flags) {
|
|
|
4289
4580
|
let promptResourceKey = null;
|
|
4290
4581
|
let basePromptOverride;
|
|
4291
4582
|
try {
|
|
4583
|
+
// Fetch the prompt from the broker the SDK client is connected to,
|
|
4584
|
+
// deriving base URL + auth from that client (not restConfig, whose base
|
|
4585
|
+
// defaults to localhost) — the resourceKey is broker-local.
|
|
4586
|
+
const promptSource = await resolveLinkedPromptSource(camunda);
|
|
4292
4587
|
const linked = await resolveLinkedPrompt(job.customHeaders ?? {}, {
|
|
4293
|
-
baseUrl: restConfig.baseUrl,
|
|
4588
|
+
baseUrl: promptSource.baseUrl || restConfig.baseUrl,
|
|
4589
|
+
authHeaders: promptSource.authHeaders,
|
|
4294
4590
|
token: restConfig.token,
|
|
4295
4591
|
});
|
|
4296
4592
|
if (linked) {
|
|
@@ -7604,6 +7900,7 @@ export { resolveBinary, findBinary, launcherEnvMarkers };
|
|
|
7604
7900
|
export { setConfig, unsetConfig, readConfig, writeConfig, getConfigFile, SETTING_ALIASES };
|
|
7605
7901
|
export { buildNpmInvocation };
|
|
7606
7902
|
export { resolveAgenticConfig, LOCAL_AGENTIC_TOKEN };
|
|
7903
|
+
export { resolveAgenticTarget, discoverAgenticHubs, probeAgenticChannel, normalizeProjectApps, isLoopbackHost };
|
|
7607
7904
|
export { compareSemver, githubRepoSlug, filterReleasesSince, renderReleaseBody };
|
|
7608
7905
|
export {
|
|
7609
7906
|
webConsoleUrl,
|
|
@@ -7620,6 +7917,8 @@ export {
|
|
|
7620
7917
|
resourceContentUrl,
|
|
7621
7918
|
fetchLinkedResourceContent,
|
|
7622
7919
|
resolveLinkedPrompt,
|
|
7920
|
+
resolveLinkedPromptSource,
|
|
7921
|
+
normalizeRestBase,
|
|
7623
7922
|
coerceBool,
|
|
7624
7923
|
coerceInt,
|
|
7625
7924
|
deepMerge,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.1",
|
|
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.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.35.1",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.35.1",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.35.1",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.35.1",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.35.1",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.35.1",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.35.1"
|
|
67
67
|
}
|
|
68
68
|
}
|