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.
Files changed (3) hide show
  1. package/README.md +29 -0
  2. package/c8ctl-plugin.js +309 -10
  3. 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
- if (token) headers.Authorization = `Bearer ${token}`;
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 agenticCfg = resolveAgenticConfig();
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.34.0",
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.34.0",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.34.0",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.34.0",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.34.0",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.34.0",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.34.0",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.34.0"
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
  }