c8ctl-plugin-nano 1.33.2 → 1.35.0
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 +54 -10
- package/c8ctl-plugin.js +295 -12
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -274,24 +274,68 @@ its agents' terminals to an operator's cockpit. This rides the app's agentic
|
|
|
274
274
|
channel (ADR 0056), served **same-port** on the app's own HTTP base URL at path
|
|
275
275
|
**`/agentic`** — not a sidecar, so there's no extra port to open.
|
|
276
276
|
|
|
277
|
-
**Connecting
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
**Connecting — on by default (local-first).** Nano is designed for local use, so
|
|
278
|
+
visibility is **on by default**. Run a worker against a local app and it appears
|
|
279
|
+
live with **zero configuration** — it joins the channel with a well-known
|
|
280
|
+
localhost token in **LOCAL mode** (no credential). The worker presents this
|
|
281
|
+
token to whatever `NANO_AGENTIC_URL` you point it at; the same-machine
|
|
282
|
+
restriction is enforced by the **hub**, which only honours the well-known LOCAL
|
|
283
|
+
token for local/loopback connections:
|
|
281
284
|
|
|
282
285
|
```bash
|
|
283
|
-
#
|
|
286
|
+
# LOCAL mode (default): appears live with no secrets
|
|
284
287
|
export NANO_AGENTIC_URL=http://localhost:8080 # app base URL; channel is served at /agentic
|
|
288
|
+
c8ctl nano work reviewer
|
|
289
|
+
# agentic channel (local): announcing presence as ‹worker› on ws://localhost:8080/agentic
|
|
290
|
+
```
|
|
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
|
+
|
|
321
|
+
**Secure mode (opt-in).** For a shared/remote deployment, enrol the worker with an
|
|
322
|
+
ADR 0028 **identity token** and a **capability credential** (the same `?token=…`
|
|
323
|
+
pattern the blackboard uses). Setting either switches the worker into SECURE mode,
|
|
324
|
+
which requires **both** (fail closed if only one is set):
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# SECURE mode: real enrolment
|
|
328
|
+
export NANO_AGENTIC_URL=http://localhost:8080
|
|
285
329
|
export NANO_AGENTIC_TOKEN=<identity-token> # ADR 0028 identity
|
|
286
330
|
export NANO_AGENTIC_CREDENTIAL=<capability-cred> # capability credential
|
|
287
331
|
c8ctl nano work reviewer
|
|
288
|
-
# agentic channel: announcing presence as ‹worker› on ws://localhost:8080/agentic
|
|
332
|
+
# agentic channel (secure): announcing presence as ‹worker› on ws://localhost:8080/agentic
|
|
289
333
|
```
|
|
290
334
|
|
|
291
|
-
|
|
292
|
-
visibility, no relay, nothing else
|
|
293
|
-
connects; an invalid identity is rejected
|
|
294
|
-
is rejected (forbidden).
|
|
335
|
+
To opt out entirely, set `NANO_AGENTIC=off` (or persisted `agentic: false`) — the
|
|
336
|
+
worker then runs with **no visibility, no relay, nothing else changed**. In secure
|
|
337
|
+
mode a valid identity + capability connects; an invalid identity is rejected
|
|
338
|
+
(unauthorized) and a missing capability is rejected (forbidden).
|
|
295
339
|
|
|
296
340
|
**How presence appears.** On connect the worker **announces** its identity, its
|
|
297
341
|
`host`, and the set of `jobKeys` it is currently running, then **heartbeats** to
|
package/c8ctl-plugin.js
CHANGED
|
@@ -135,6 +135,16 @@ const SUPERVISOR_STATE_FILE = 'supervisor.json';
|
|
|
135
135
|
const PROCESSOS_DEFAULT_PORT = 8090;
|
|
136
136
|
const DEFAULT_NANO_URL = 'http://localhost:8080';
|
|
137
137
|
|
|
138
|
+
// The well-known identity token used for LOCAL agentic visibility (security opt-in). Nano is
|
|
139
|
+
// local-first: on the operator's own machine a `nano work` worker joins the visibility channel with
|
|
140
|
+
// zero configuration, so it presents this constant, well-known localhost token — NOT a secret. The
|
|
141
|
+
// worker does not enforce any loopback restriction itself (it presents this token to whatever
|
|
142
|
+
// NANO_AGENTIC_URL is configured); same-machine gating is enforced by the hub, which only honours
|
|
143
|
+
// this well-known token for local/loopback connections. Kept in lock-step with the hub constant in
|
|
144
|
+
// nanobpm/nano-workforce (`app/agentic/channel.ts` LOCAL_AGENTIC_TOKEN). In secure mode (a real
|
|
145
|
+
// NANO_AGENTIC_TOKEN + NANO_AGENTIC_CREDENTIAL) this is never used.
|
|
146
|
+
const LOCAL_AGENTIC_TOKEN = 'nano-local';
|
|
147
|
+
|
|
138
148
|
// Passive update notifier (npm-style): refresh the latest published version
|
|
139
149
|
// from the registry in a detached background process at most once per day, and
|
|
140
150
|
// surface a one-line "update available" notice at most once per day. Never
|
|
@@ -3800,30 +3810,278 @@ function buildResultEnvelope(result, { sandbox, image, git, result: agentResult,
|
|
|
3800
3810
|
* HTTP base URL at `/agentic`; the identity token + capability credential follow
|
|
3801
3811
|
* the blackboard's `?token=…` pattern.
|
|
3802
3812
|
*
|
|
3813
|
+
* Local-first (security opt-in). Nano is designed for local use, so visibility is
|
|
3814
|
+
* ON BY DEFAULT:
|
|
3815
|
+
* - LOCAL mode (default): no credentials configured — the worker connects with
|
|
3816
|
+
* the well-known localhost token ({@link LOCAL_AGENTIC_TOKEN}) and no
|
|
3817
|
+
* capability credential, so it appears live with zero configuration (the hub's
|
|
3818
|
+
* matching LOCAL mode accepts it).
|
|
3819
|
+
* - SECURE mode: set NANO_AGENTIC_TOKEN + NANO_AGENTIC_CREDENTIAL (or the
|
|
3820
|
+
* persisted `agenticToken`/`agenticCredential`) — an ADR 0028 identity token
|
|
3821
|
+
* AND a capability credential are then sent (enrolment). If only one is set the
|
|
3822
|
+
* config is incomplete and we stay off (fail closed), returning `null`.
|
|
3823
|
+
* - OFF: NANO_AGENTIC=off (or 0/false/no), or persisted `agentic:false`.
|
|
3824
|
+
*
|
|
3803
3825
|
* Env wins over persisted config; the base URL falls back to the configured nano
|
|
3804
|
-
* URL (the app's own port).
|
|
3805
|
-
* and a capability credential are present (enrolment) — absent either, it runs
|
|
3806
|
-
* exactly as before, off the visibility page. Returns `null` when not enrolled.
|
|
3826
|
+
* URL (the app's own port). Returns `null` only when disabled or half-configured.
|
|
3807
3827
|
*
|
|
3808
|
-
* @returns {{ url: string, token: string, credential: string, bufferCapacity: number } | null}
|
|
3828
|
+
* @returns {{ url: string, token: string, credential: string, bufferCapacity: number, secure: boolean } | null}
|
|
3809
3829
|
*/
|
|
3810
3830
|
function resolveAgenticConfig() {
|
|
3811
3831
|
const cfg = readConfig();
|
|
3832
|
+
// Explicit off-switch (env wins). Lets an operator fully opt out of visibility.
|
|
3833
|
+
const offSetting = process.env.NANO_AGENTIC
|
|
3834
|
+
?? (cfg.agentic === false ? 'off' : cfg.agentic);
|
|
3835
|
+
if (/^(0|off|false|no)$/i.test(String(offSetting ?? ''))) return null;
|
|
3836
|
+
|
|
3837
|
+
// An explicit agentic target (env NANO_AGENTIC_URL or persisted `agenticUrl`)
|
|
3838
|
+
// is used verbatim and short-circuits hub auto-discovery (#75). When neither is
|
|
3839
|
+
// set the URL below is the ENGINE base (nanoUrl → NANO_BASE_URL → default), off
|
|
3840
|
+
// which `resolveAgenticTarget()` discovers the embedded app's own /agentic port.
|
|
3841
|
+
const explicitUrl = !!(process.env.NANO_AGENTIC_URL || cfg.agenticUrl);
|
|
3812
3842
|
const url = process.env.NANO_AGENTIC_URL
|
|
3813
3843
|
|| cfg.agenticUrl
|
|
3814
3844
|
|| cfg.nanoUrl
|
|
3815
3845
|
|| process.env.NANO_BASE_URL
|
|
3816
3846
|
|| DEFAULT_NANO_URL;
|
|
3847
|
+
if (!url) return null;
|
|
3817
3848
|
const token = process.env.NANO_AGENTIC_TOKEN || cfg.agenticToken || '';
|
|
3818
3849
|
const credential = process.env.NANO_AGENTIC_CREDENTIAL || cfg.agenticCredential || '';
|
|
3819
|
-
if (!url || !token || !credential) return null;
|
|
3820
3850
|
// Outbound hub-down buffer bound (frames). Operator-tunable (C4, #43) so a
|
|
3821
3851
|
// long expected outage can be given more headroom; resolveBufferCapacity
|
|
3822
3852
|
// validates it to a positive integer and falls back to the client default.
|
|
3823
3853
|
const bufferCapacity = resolveBufferCapacity(
|
|
3824
3854
|
process.env.NANO_AGENTIC_BUFFER_CAPACITY ?? cfg.agenticBufferCapacity,
|
|
3825
3855
|
);
|
|
3826
|
-
|
|
3856
|
+
|
|
3857
|
+
// SECURE mode: any explicit credential configured means the operator opted into
|
|
3858
|
+
// enrolment — require BOTH halves, fail closed if only one is present.
|
|
3859
|
+
if (token || credential) {
|
|
3860
|
+
if (!token || !credential) return null;
|
|
3861
|
+
return { url, token, credential, bufferCapacity, secure: true, explicitUrl };
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
// LOCAL mode (default): well-known localhost token, no capability credential.
|
|
3865
|
+
return { url, token: LOCAL_AGENTIC_TOKEN, credential: '', bufferCapacity, secure: false, explicitUrl };
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// Total budget for zero-config hub auto-discovery (#75): the projects-API read
|
|
3869
|
+
// and every WS upgrade probe must degrade to a "not discoverable" advisory
|
|
3870
|
+
// within this window so discovery never meaningfully delays job polling.
|
|
3871
|
+
const AGENTIC_DISCOVERY_TIMEOUT_MS = 2_000;
|
|
3872
|
+
|
|
3873
|
+
/**
|
|
3874
|
+
* Is `hostname` a loopback address? Discovery reads the engine's projects API
|
|
3875
|
+
* and then probes `127.0.0.1:<advertised port>`, so it must only run against a
|
|
3876
|
+
* loopback engine — otherwise a remote engine's response would steer a local
|
|
3877
|
+
* port probe, breaking the loopback-only guarantee (#75/#76). Accepts
|
|
3878
|
+
* `localhost`, the IPv4 loopback block `127.0.0.0/8`, and IPv6 `::1` (with or
|
|
3879
|
+
* without URL brackets); case-insensitive and trimmed.
|
|
3880
|
+
*
|
|
3881
|
+
* @param {string} hostname a URL hostname (e.g. `127.0.0.1`, `localhost`, `[::1]`)
|
|
3882
|
+
* @returns {boolean}
|
|
3883
|
+
*/
|
|
3884
|
+
function isLoopbackHost(hostname) {
|
|
3885
|
+
const h = String(hostname || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
3886
|
+
if (!h) return false;
|
|
3887
|
+
if (h === 'localhost' || h === '::1') return true;
|
|
3888
|
+
return /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
/**
|
|
3892
|
+
* Normalise the engine's `GET /console/api/projects` payload into the running
|
|
3893
|
+
* embedded apps that advertise an agentic UI port. Accepts the shapes the
|
|
3894
|
+
* console may serve — a keyed map (`{ "Nano_Workforce": { appUi } }`), a bare
|
|
3895
|
+
* array (`[{ name, appUi }]`), or a wrapped array (`{ projects: [...] }`) — and
|
|
3896
|
+
* keeps only apps with `appUi.enabled === true` and a positive integer
|
|
3897
|
+
* `appUi.port`. Anything else (a Camunda engine, an API-only gateway, malformed
|
|
3898
|
+
* JSON) yields `[]`.
|
|
3899
|
+
*
|
|
3900
|
+
* @param {unknown} projects the parsed projects-API body
|
|
3901
|
+
* @returns {Array<{ project: string, port: number, label?: string }>}
|
|
3902
|
+
*/
|
|
3903
|
+
function normalizeProjectApps(projects) {
|
|
3904
|
+
if (!projects || typeof projects !== 'object') return [];
|
|
3905
|
+
let entries;
|
|
3906
|
+
if (Array.isArray(projects)) {
|
|
3907
|
+
entries = projects.map((p) => [p?.name ?? p?.project ?? p?.id, p]);
|
|
3908
|
+
} else if (Array.isArray(projects.projects)) {
|
|
3909
|
+
entries = projects.projects.map((p) => [p?.name ?? p?.project ?? p?.id, p]);
|
|
3910
|
+
} else {
|
|
3911
|
+
entries = Object.entries(projects);
|
|
3912
|
+
}
|
|
3913
|
+
const apps = [];
|
|
3914
|
+
for (const [name, proj] of entries) {
|
|
3915
|
+
const ui = proj?.appUi;
|
|
3916
|
+
if (ui && ui.enabled === true && Number.isInteger(ui.port) && ui.port > 0) {
|
|
3917
|
+
apps.push({
|
|
3918
|
+
project: String(name ?? ui.label ?? ui.port),
|
|
3919
|
+
port: ui.port,
|
|
3920
|
+
label: ui.label,
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
return apps;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
/**
|
|
3928
|
+
* Probe whether an embedded app's own loopback `/agentic` endpoint answers a
|
|
3929
|
+
* WebSocket upgrade. Connects to `ws://127.0.0.1:<port>/agentic?token=…` and
|
|
3930
|
+
* resolves `true` only if the socket opens within `timeoutMs`; a refused
|
|
3931
|
+
* connection, the console proxy's deliberate `501`, a `404`, or a timeout all
|
|
3932
|
+
* resolve `false`. Loopback-only and self-cleaning — the probe socket is closed
|
|
3933
|
+
* as soon as the outcome is known. Never throws.
|
|
3934
|
+
*
|
|
3935
|
+
* @param {number} port the app's direct loopback port (`appUi.port`)
|
|
3936
|
+
* @param {{ token?: string, WebSocketImpl?: Function, timeoutMs?: number }} [opts]
|
|
3937
|
+
* @returns {Promise<boolean>}
|
|
3938
|
+
*/
|
|
3939
|
+
function probeAgenticChannel(port, {
|
|
3940
|
+
token = LOCAL_AGENTIC_TOKEN,
|
|
3941
|
+
WebSocketImpl = globalThis.WebSocket,
|
|
3942
|
+
timeoutMs = AGENTIC_DISCOVERY_TIMEOUT_MS,
|
|
3943
|
+
} = {}) {
|
|
3944
|
+
if (typeof WebSocketImpl !== 'function') return Promise.resolve(false);
|
|
3945
|
+
const url = `ws://127.0.0.1:${port}/agentic?token=${encodeURIComponent(token)}`;
|
|
3946
|
+
return new Promise((resolve) => {
|
|
3947
|
+
let done = false;
|
|
3948
|
+
let ws;
|
|
3949
|
+
const finish = (ok) => {
|
|
3950
|
+
if (done) return;
|
|
3951
|
+
done = true;
|
|
3952
|
+
clearTimeout(timer);
|
|
3953
|
+
try { ws?.close(); } catch { /* best-effort cleanup */ }
|
|
3954
|
+
resolve(ok);
|
|
3955
|
+
};
|
|
3956
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
3957
|
+
try {
|
|
3958
|
+
ws = new WebSocketImpl(url);
|
|
3959
|
+
ws.onopen = () => finish(true);
|
|
3960
|
+
ws.onerror = () => finish(false);
|
|
3961
|
+
ws.onclose = () => finish(false);
|
|
3962
|
+
} catch {
|
|
3963
|
+
finish(false);
|
|
3964
|
+
}
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
/**
|
|
3969
|
+
* Auto-discover the embedded nwf agentic hub(s) reachable from an engine base
|
|
3970
|
+
* URL (#75). Reads `GET <engine>/console/api/projects`, keeps the apps that
|
|
3971
|
+
* advertise an agentic UI port, and WS-probes each app's direct loopback
|
|
3972
|
+
* `/agentic` to confirm the channel is actually served there (bypassing the
|
|
3973
|
+
* WS-incapable console proxy). Loopback-only (the engine host itself must be
|
|
3974
|
+
* loopback, since the response steers a local port probe), enforces a single
|
|
3975
|
+
* shared time budget across the fetch + probes, and is fail-open: any error —
|
|
3976
|
+
* not a nano engine (Camunda), a non-loopback engine, network failure,
|
|
3977
|
+
* malformed body, or an overall timeout — degrades to `[]` so the worker's real
|
|
3978
|
+
* job is never blocked.
|
|
3979
|
+
*
|
|
3980
|
+
* @param {string} engineBaseUrl the engine base URL (e.g. `http://localhost:8080`)
|
|
3981
|
+
* @param {{ token?: string, fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
|
|
3982
|
+
* @returns {Promise<Array<{ project: string, port: number, label?: string }>>}
|
|
3983
|
+
*/
|
|
3984
|
+
async function discoverAgenticHubs(engineBaseUrl, {
|
|
3985
|
+
token = LOCAL_AGENTIC_TOKEN,
|
|
3986
|
+
fetchImpl = globalThis.fetch,
|
|
3987
|
+
wsProbe = probeAgenticChannel,
|
|
3988
|
+
timeoutMs = AGENTIC_DISCOVERY_TIMEOUT_MS,
|
|
3989
|
+
} = {}) {
|
|
3990
|
+
if (typeof fetchImpl !== 'function' || typeof engineBaseUrl !== 'string' || !engineBaseUrl.trim()) {
|
|
3991
|
+
return [];
|
|
3992
|
+
}
|
|
3993
|
+
const base = engineBaseUrl.replace(/\/+$/, '');
|
|
3994
|
+
// Loopback-only: discovery probes 127.0.0.1:<port> using a port advertised by
|
|
3995
|
+
// the engine's projects API, so a non-loopback (remote) engine could steer a
|
|
3996
|
+
// local port probe. Refuse discovery unless the engine host is loopback (#76).
|
|
3997
|
+
let host;
|
|
3998
|
+
try {
|
|
3999
|
+
host = new URL(base).hostname;
|
|
4000
|
+
} catch {
|
|
4001
|
+
return [];
|
|
4002
|
+
}
|
|
4003
|
+
if (!isLoopbackHost(host)) return [];
|
|
4004
|
+
// Single discovery budget: the projects fetch and the WS probes share ONE
|
|
4005
|
+
// deadline, so total discovery can't approach 2× timeoutMs (the fetch could
|
|
4006
|
+
// consume ~timeoutMs and then each probe was previously given a fresh full
|
|
4007
|
+
// budget). Probes get only the time left after the fetch (#76).
|
|
4008
|
+
const deadline = Date.now() + timeoutMs;
|
|
4009
|
+
let projects;
|
|
4010
|
+
const controller = new AbortController();
|
|
4011
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4012
|
+
try {
|
|
4013
|
+
const res = await fetchImpl(`${base}/console/api/projects`, { signal: controller.signal });
|
|
4014
|
+
if (!res || !res.ok) return [];
|
|
4015
|
+
projects = await res.json();
|
|
4016
|
+
} catch {
|
|
4017
|
+
return [];
|
|
4018
|
+
} finally {
|
|
4019
|
+
clearTimeout(timer);
|
|
4020
|
+
}
|
|
4021
|
+
const apps = normalizeProjectApps(projects);
|
|
4022
|
+
if (apps.length === 0) return [];
|
|
4023
|
+
const remainingMs = deadline - Date.now();
|
|
4024
|
+
if (remainingMs <= 0) return [];
|
|
4025
|
+
// Probe candidate ports concurrently within the remaining shared budget.
|
|
4026
|
+
const settled = await Promise.all(apps.map(async (app) => {
|
|
4027
|
+
try {
|
|
4028
|
+
return (await wsProbe(app.port, { token, timeoutMs: remainingMs })) ? app : null;
|
|
4029
|
+
} catch {
|
|
4030
|
+
return null;
|
|
4031
|
+
}
|
|
4032
|
+
}));
|
|
4033
|
+
return settled.filter(Boolean);
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
/**
|
|
4037
|
+
* Resolve the final agentic-channel target for a worker, running zero-config hub
|
|
4038
|
+
* auto-discovery when no explicit target is configured (#75). Layered on top of
|
|
4039
|
+
* {@link resolveAgenticConfig}:
|
|
4040
|
+
*
|
|
4041
|
+
* - `{ status: 'off' }` — visibility disabled (off-switch) or SECURE mode
|
|
4042
|
+
* half-configured. No discovery attempted.
|
|
4043
|
+
* - `{ status: 'connect', config }` — a target to connect to. Either the
|
|
4044
|
+
* explicit `NANO_AGENTIC_URL`/`agenticUrl` verbatim (no discovery), or the
|
|
4045
|
+
* single discovered app's direct `ws://127.0.0.1:<port>/agentic` loopback.
|
|
4046
|
+
* - `{ status: 'ambiguous', message, candidates }` — two+ apps expose a
|
|
4047
|
+
* channel. Hard stop for the worker: it must not silently pick one.
|
|
4048
|
+
* - `{ status: 'advisory', message }` — nothing discoverable (zero matches,
|
|
4049
|
+
* no projects API / not a nano engine, or discovery error/timeout). The
|
|
4050
|
+
* worker continues doing real work with no channel.
|
|
4051
|
+
*
|
|
4052
|
+
* @param {{ fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
|
|
4053
|
+
* @returns {Promise<{ status: string, config?: object, message?: string, candidates?: Array }>}
|
|
4054
|
+
*/
|
|
4055
|
+
async function resolveAgenticTarget(opts = {}) {
|
|
4056
|
+
const base = resolveAgenticConfig();
|
|
4057
|
+
if (!base) return { status: 'off' };
|
|
4058
|
+
// Explicit target wins verbatim and skips discovery entirely.
|
|
4059
|
+
if (base.explicitUrl) return { status: 'connect', config: base };
|
|
4060
|
+
|
|
4061
|
+
const hubs = await discoverAgenticHubs(base.url, { token: base.token, ...opts });
|
|
4062
|
+
|
|
4063
|
+
if (hubs.length === 1) {
|
|
4064
|
+
const { project, port } = hubs[0];
|
|
4065
|
+
return {
|
|
4066
|
+
status: 'connect',
|
|
4067
|
+
config: { ...base, url: `http://127.0.0.1:${port}`, discovered: { project, port } },
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
4070
|
+
if (hubs.length > 1) {
|
|
4071
|
+
const list = hubs.map((h) => `${h.project} → :${h.port}`).join(', ');
|
|
4072
|
+
return {
|
|
4073
|
+
status: 'ambiguous',
|
|
4074
|
+
candidates: hubs,
|
|
4075
|
+
message: `multiple embedded apps expose an agentic channel (${list}); refusing to guess. `
|
|
4076
|
+
+ 'Disambiguate by setting NANO_AGENTIC_URL=http://127.0.0.1:<port> (or persisted agenticUrl) to the one you want.',
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
return {
|
|
4080
|
+
status: 'advisory',
|
|
4081
|
+
message: `agentic visibility was not discoverable at ${base.url} — the embedded app port could `
|
|
4082
|
+
+ 'not be found (not a nano engine, or its console projects API is absent). Set '
|
|
4083
|
+
+ 'NANO_AGENTIC_URL=http://127.0.0.1:<appUi.port> to enable the visibility channel. Continuing without it.',
|
|
4084
|
+
};
|
|
3827
4085
|
}
|
|
3828
4086
|
|
|
3829
4087
|
/**
|
|
@@ -4146,9 +4404,30 @@ async function workAgent(req, flags) {
|
|
|
4146
4404
|
// accessors on `workChannel` (relay-lane sink + connect/disconnect/reconnect
|
|
4147
4405
|
// lifecycle events) rather than opening their own connection.
|
|
4148
4406
|
//
|
|
4149
|
-
//
|
|
4150
|
-
// worker
|
|
4151
|
-
|
|
4407
|
+
// Local-first (security opt-in): visibility is ON BY DEFAULT. In LOCAL mode the
|
|
4408
|
+
// worker joins with the well-known localhost token and no credential; SECURE
|
|
4409
|
+
// mode (NANO_AGENTIC_TOKEN + NANO_AGENTIC_CREDENTIAL) sends a real ADR 0028
|
|
4410
|
+
// identity + capability; NANO_AGENTIC=off disables it (see resolveAgenticConfig).
|
|
4411
|
+
const agenticTarget = await resolveAgenticTarget({ logger });
|
|
4412
|
+
let agenticCfg = null;
|
|
4413
|
+
switch (agenticTarget.status) {
|
|
4414
|
+
case 'connect':
|
|
4415
|
+
agenticCfg = agenticTarget.config;
|
|
4416
|
+
break;
|
|
4417
|
+
case 'ambiguous':
|
|
4418
|
+
// The operator ran with visibility on-by-default but the hub is
|
|
4419
|
+
// unresolved — a misconfiguration to fix, not to guess through. Hard stop.
|
|
4420
|
+
logger.error(` agentic visibility is ambiguous: ${agenticTarget.message}`);
|
|
4421
|
+
process.exit(1);
|
|
4422
|
+
break;
|
|
4423
|
+
case 'advisory':
|
|
4424
|
+
logger.info(` agentic channel: ${agenticTarget.message}`);
|
|
4425
|
+
break;
|
|
4426
|
+
case 'off':
|
|
4427
|
+
default:
|
|
4428
|
+
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.');
|
|
4429
|
+
break;
|
|
4430
|
+
}
|
|
4152
4431
|
if (agenticCfg) {
|
|
4153
4432
|
try {
|
|
4154
4433
|
workChannel = await createWorkChannel({
|
|
@@ -4167,7 +4446,11 @@ async function workAgent(req, flags) {
|
|
|
4167
4446
|
logger,
|
|
4168
4447
|
});
|
|
4169
4448
|
const shown = redactAgenticUrl(buildAgenticUrl(agenticCfg.url, {}));
|
|
4170
|
-
|
|
4449
|
+
const mode = agenticCfg.secure ? 'secure' : 'local';
|
|
4450
|
+
if (agenticCfg.discovered) {
|
|
4451
|
+
logger.info(` agentic channel: auto-discovered ${agenticCfg.discovered.project} on the embedded app port :${agenticCfg.discovered.port} (bypassing the WS-incapable console proxy).`);
|
|
4452
|
+
}
|
|
4453
|
+
logger.info(` agentic channel (${mode}): announcing presence as ${workerName} on ${shown}`);
|
|
4171
4454
|
} catch (err) {
|
|
4172
4455
|
// Never let a channel failure stop the worker from doing its actual job.
|
|
4173
4456
|
workChannel = null;
|
|
@@ -4190,8 +4473,6 @@ async function workAgent(req, flags) {
|
|
|
4190
4473
|
logger.warn(` agentic buffer monitor unavailable (${err?.message || err}); channel presence still active.`);
|
|
4191
4474
|
}
|
|
4192
4475
|
}
|
|
4193
|
-
} else {
|
|
4194
|
-
logger.info(' agentic channel: not enrolled (set NANO_AGENTIC_URL + NANO_AGENTIC_TOKEN + NANO_AGENTIC_CREDENTIAL to appear on the visibility page).');
|
|
4195
4476
|
}
|
|
4196
4477
|
|
|
4197
4478
|
// C3 (#42): the role's live-terminal mode — a full PTY (streamed on the relay
|
|
@@ -7566,6 +7847,8 @@ function parseProcessosRequest(args, flags) {
|
|
|
7566
7847
|
export { resolveBinary, findBinary, launcherEnvMarkers };
|
|
7567
7848
|
export { setConfig, unsetConfig, readConfig, writeConfig, getConfigFile, SETTING_ALIASES };
|
|
7568
7849
|
export { buildNpmInvocation };
|
|
7850
|
+
export { resolveAgenticConfig, LOCAL_AGENTIC_TOKEN };
|
|
7851
|
+
export { resolveAgenticTarget, discoverAgenticHubs, probeAgenticChannel, normalizeProjectApps, isLoopbackHost };
|
|
7569
7852
|
export { compareSemver, githubRepoSlug, filterReleasesSince, renderReleaseBody };
|
|
7570
7853
|
export {
|
|
7571
7854
|
webConsoleUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
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.0",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.35.0",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.35.0",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.35.0",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.35.0",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.35.0",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.35.0"
|
|
67
67
|
}
|
|
68
68
|
}
|