c8ctl-plugin-nano 1.33.1 → 1.34.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 +25 -10
- package/c8ctl-plugin.js +87 -16
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -274,24 +274,39 @@ 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
|
+
**Secure mode (opt-in).** For a shared/remote deployment, enrol the worker with an
|
|
293
|
+
ADR 0028 **identity token** and a **capability credential** (the same `?token=…`
|
|
294
|
+
pattern the blackboard uses). Setting either switches the worker into SECURE mode,
|
|
295
|
+
which requires **both** (fail closed if only one is set):
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
# SECURE mode: real enrolment
|
|
299
|
+
export NANO_AGENTIC_URL=http://localhost:8080
|
|
285
300
|
export NANO_AGENTIC_TOKEN=<identity-token> # ADR 0028 identity
|
|
286
301
|
export NANO_AGENTIC_CREDENTIAL=<capability-cred> # capability credential
|
|
287
302
|
c8ctl nano work reviewer
|
|
288
|
-
# agentic channel: announcing presence as ‹worker› on ws://localhost:8080/agentic
|
|
303
|
+
# agentic channel (secure): announcing presence as ‹worker› on ws://localhost:8080/agentic
|
|
289
304
|
```
|
|
290
305
|
|
|
291
|
-
|
|
292
|
-
visibility, no relay, nothing else
|
|
293
|
-
connects; an invalid identity is rejected
|
|
294
|
-
is rejected (forbidden).
|
|
306
|
+
To opt out entirely, set `NANO_AGENTIC=off` (or persisted `agentic: false`) — the
|
|
307
|
+
worker then runs with **no visibility, no relay, nothing else changed**. In secure
|
|
308
|
+
mode a valid identity + capability connects; an invalid identity is rejected
|
|
309
|
+
(unauthorized) and a missing capability is rejected (forbidden).
|
|
295
310
|
|
|
296
311
|
**How presence appears.** On connect the worker **announces** its identity, its
|
|
297
312
|
`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
|
|
@@ -161,6 +171,17 @@ function getLogger() {
|
|
|
161
171
|
warn: console.warn,
|
|
162
172
|
error: console.error,
|
|
163
173
|
debug: () => {},
|
|
174
|
+
// Primary command output, written to stdout as-is (mirrors the c8ctl host
|
|
175
|
+
// logger's `output()`). Used for preformatted, non-structured content such
|
|
176
|
+
// as the supervisor status table, whose newlines must survive verbatim.
|
|
177
|
+
// Uses `process.stdout.write` (not `console.log`) so the content is emitted
|
|
178
|
+
// literally — `console.log` applies `util.format` (mangling stray `%`
|
|
179
|
+
// sequences) and would append its own newline; here we add exactly one
|
|
180
|
+
// trailing newline when the text lacks one.
|
|
181
|
+
output: (msg) => {
|
|
182
|
+
const s = typeof msg === 'string' ? msg : String(msg);
|
|
183
|
+
process.stdout.write(s.endsWith('\n') ? s : s + '\n');
|
|
184
|
+
},
|
|
164
185
|
};
|
|
165
186
|
}
|
|
166
187
|
|
|
@@ -1244,7 +1265,7 @@ function logsCluster(req) {
|
|
|
1244
1265
|
const proc = spawn('tail', tailArgs, { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
1245
1266
|
proc.on('error', (err) => {
|
|
1246
1267
|
logger.error(`Failed to read logs: ${err.message}`);
|
|
1247
|
-
logger.
|
|
1268
|
+
logger.output(`Log files:\n ${files.join('\n ')}`);
|
|
1248
1269
|
});
|
|
1249
1270
|
}
|
|
1250
1271
|
|
|
@@ -3789,30 +3810,54 @@ function buildResultEnvelope(result, { sandbox, image, git, result: agentResult,
|
|
|
3789
3810
|
* HTTP base URL at `/agentic`; the identity token + capability credential follow
|
|
3790
3811
|
* the blackboard's `?token=…` pattern.
|
|
3791
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
|
+
*
|
|
3792
3825
|
* Env wins over persisted config; the base URL falls back to the configured nano
|
|
3793
|
-
* URL (the app's own port).
|
|
3794
|
-
* and a capability credential are present (enrolment) — absent either, it runs
|
|
3795
|
-
* 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.
|
|
3796
3827
|
*
|
|
3797
|
-
* @returns {{ url: string, token: string, credential: string, bufferCapacity: number } | null}
|
|
3828
|
+
* @returns {{ url: string, token: string, credential: string, bufferCapacity: number, secure: boolean } | null}
|
|
3798
3829
|
*/
|
|
3799
3830
|
function resolveAgenticConfig() {
|
|
3800
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
|
+
|
|
3801
3837
|
const url = process.env.NANO_AGENTIC_URL
|
|
3802
3838
|
|| cfg.agenticUrl
|
|
3803
3839
|
|| cfg.nanoUrl
|
|
3804
3840
|
|| process.env.NANO_BASE_URL
|
|
3805
3841
|
|| DEFAULT_NANO_URL;
|
|
3842
|
+
if (!url) return null;
|
|
3806
3843
|
const token = process.env.NANO_AGENTIC_TOKEN || cfg.agenticToken || '';
|
|
3807
3844
|
const credential = process.env.NANO_AGENTIC_CREDENTIAL || cfg.agenticCredential || '';
|
|
3808
|
-
if (!url || !token || !credential) return null;
|
|
3809
3845
|
// Outbound hub-down buffer bound (frames). Operator-tunable (C4, #43) so a
|
|
3810
3846
|
// long expected outage can be given more headroom; resolveBufferCapacity
|
|
3811
3847
|
// validates it to a positive integer and falls back to the client default.
|
|
3812
3848
|
const bufferCapacity = resolveBufferCapacity(
|
|
3813
3849
|
process.env.NANO_AGENTIC_BUFFER_CAPACITY ?? cfg.agenticBufferCapacity,
|
|
3814
3850
|
);
|
|
3815
|
-
|
|
3851
|
+
|
|
3852
|
+
// SECURE mode: any explicit credential configured means the operator opted into
|
|
3853
|
+
// enrolment — require BOTH halves, fail closed if only one is present.
|
|
3854
|
+
if (token || credential) {
|
|
3855
|
+
if (!token || !credential) return null;
|
|
3856
|
+
return { url, token, credential, bufferCapacity, secure: true };
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
// LOCAL mode (default): well-known localhost token, no capability credential.
|
|
3860
|
+
return { url, token: LOCAL_AGENTIC_TOKEN, credential: '', bufferCapacity, secure: false };
|
|
3816
3861
|
}
|
|
3817
3862
|
|
|
3818
3863
|
/**
|
|
@@ -4135,8 +4180,10 @@ async function workAgent(req, flags) {
|
|
|
4135
4180
|
// accessors on `workChannel` (relay-lane sink + connect/disconnect/reconnect
|
|
4136
4181
|
// lifecycle events) rather than opening their own connection.
|
|
4137
4182
|
//
|
|
4138
|
-
//
|
|
4139
|
-
// worker
|
|
4183
|
+
// Local-first (security opt-in): visibility is ON BY DEFAULT. In LOCAL mode the
|
|
4184
|
+
// worker joins with the well-known localhost token and no credential; SECURE
|
|
4185
|
+
// mode (NANO_AGENTIC_TOKEN + NANO_AGENTIC_CREDENTIAL) sends a real ADR 0028
|
|
4186
|
+
// identity + capability; NANO_AGENTIC=off disables it (see resolveAgenticConfig).
|
|
4140
4187
|
const agenticCfg = resolveAgenticConfig();
|
|
4141
4188
|
if (agenticCfg) {
|
|
4142
4189
|
try {
|
|
@@ -4156,7 +4203,8 @@ async function workAgent(req, flags) {
|
|
|
4156
4203
|
logger,
|
|
4157
4204
|
});
|
|
4158
4205
|
const shown = redactAgenticUrl(buildAgenticUrl(agenticCfg.url, {}));
|
|
4159
|
-
|
|
4206
|
+
const mode = agenticCfg.secure ? 'secure' : 'local';
|
|
4207
|
+
logger.info(` agentic channel (${mode}): announcing presence as ${workerName} on ${shown}`);
|
|
4160
4208
|
} catch (err) {
|
|
4161
4209
|
// Never let a channel failure stop the worker from doing its actual job.
|
|
4162
4210
|
workChannel = null;
|
|
@@ -4180,7 +4228,7 @@ async function workAgent(req, flags) {
|
|
|
4180
4228
|
}
|
|
4181
4229
|
}
|
|
4182
4230
|
} else {
|
|
4183
|
-
logger.info(' agentic channel:
|
|
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.');
|
|
4184
4232
|
}
|
|
4185
4233
|
|
|
4186
4234
|
// C3 (#42): the role's live-terminal mode — a full PTY (streamed on the relay
|
|
@@ -5094,6 +5142,27 @@ function formatSupervisorStatus(status) {
|
|
|
5094
5142
|
return lines.join('\n');
|
|
5095
5143
|
}
|
|
5096
5144
|
|
|
5145
|
+
/**
|
|
5146
|
+
* Print a preformatted supervisor status table as primary command output.
|
|
5147
|
+
*
|
|
5148
|
+
* Preformatted, multi-line text MUST go through the logger's `output()`
|
|
5149
|
+
* channel, never `info()`. In `--output json` mode the c8ctl host logger wraps
|
|
5150
|
+
* an `info()` message in a JSON envelope (`{"status":"info","message":"…"}`),
|
|
5151
|
+
* which escapes every newline to a literal `\n` and collapses the aligned table
|
|
5152
|
+
* onto a single line — the exact breakage this guards against. `output()` writes
|
|
5153
|
+
* the content to stdout verbatim in every output mode (like `raw` command
|
|
5154
|
+
* output), so the table renders correctly regardless of mode. Falls back to
|
|
5155
|
+
* `info()` for a logger without `output()`, and to `console.log` if `logger`
|
|
5156
|
+
* is null/undefined or lacks `info()` (defensive; both the c8ctl host logger
|
|
5157
|
+
* and this plugin's fallback logger provide `output()`).
|
|
5158
|
+
*/
|
|
5159
|
+
function printSupervisorStatus(logger, status) {
|
|
5160
|
+
const text = formatSupervisorStatus(status);
|
|
5161
|
+
if (logger && typeof logger.output === 'function') logger.output(text);
|
|
5162
|
+
else if (logger && typeof logger.info === 'function') logger.info(text);
|
|
5163
|
+
else console.log(text);
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5097
5166
|
function readSupervisorState() {
|
|
5098
5167
|
const file = getSupervisorStateFile();
|
|
5099
5168
|
if (!existsSync(file)) return null;
|
|
@@ -5727,7 +5796,7 @@ async function supervisorStatusCmd() {
|
|
|
5727
5796
|
const res = await supervisorRequest({ op: 'status' }, { socketPath: getSupervisorSocketPath(), timeoutMs: 500, responseTimeoutMs: SUPERVISOR_PROBE_RESPONSE_TIMEOUT_MS });
|
|
5728
5797
|
if (res && res.ok) {
|
|
5729
5798
|
try { writeSupervisorState(stateFromStatus(res, getSupervisorSocketPath())); } catch { /* best effort */ }
|
|
5730
|
-
logger
|
|
5799
|
+
printSupervisorStatus(logger, res);
|
|
5731
5800
|
return;
|
|
5732
5801
|
}
|
|
5733
5802
|
} catch { /* no live daemon on the socket — genuinely down */ }
|
|
@@ -5743,13 +5812,13 @@ async function supervisorStatusCmd() {
|
|
|
5743
5812
|
}
|
|
5744
5813
|
try {
|
|
5745
5814
|
const res = await supervisorRequest({ op: 'status' });
|
|
5746
|
-
if (res.ok) { logger
|
|
5815
|
+
if (res.ok) { printSupervisorStatus(logger, res); return; }
|
|
5747
5816
|
} catch { /* fall back to state file below */ }
|
|
5748
5817
|
// Socket unreachable but pid alive — render from the last persisted state.
|
|
5749
|
-
logger
|
|
5818
|
+
printSupervisorStatus(logger, {
|
|
5750
5819
|
daemon: { pid: running.pid, startedAt: running.startedAt, socket: running.socket },
|
|
5751
5820
|
workers: (running.workers || []).map((w) => summarizeSupervisorWorker(w)),
|
|
5752
|
-
})
|
|
5821
|
+
});
|
|
5753
5822
|
}
|
|
5754
5823
|
|
|
5755
5824
|
async function supervisorAddCmd(req, flags) {
|
|
@@ -5862,7 +5931,7 @@ function supervisorLogsCmd(req) {
|
|
|
5862
5931
|
if (follow) logger.warn('`--follow` is not supported without `tail` on this platform; printing the current tail only.');
|
|
5863
5932
|
try {
|
|
5864
5933
|
const lines = readFileSync(file, 'utf-8').split('\n');
|
|
5865
|
-
logger.
|
|
5934
|
+
logger.output(lines.slice(-200).join('\n'));
|
|
5866
5935
|
} catch (err) { logger.error(`Could not read ${file}: ${err.message}`); }
|
|
5867
5936
|
});
|
|
5868
5937
|
}
|
|
@@ -7534,6 +7603,7 @@ function parseProcessosRequest(args, flags) {
|
|
|
7534
7603
|
export { resolveBinary, findBinary, launcherEnvMarkers };
|
|
7535
7604
|
export { setConfig, unsetConfig, readConfig, writeConfig, getConfigFile, SETTING_ALIASES };
|
|
7536
7605
|
export { buildNpmInvocation };
|
|
7606
|
+
export { resolveAgenticConfig, LOCAL_AGENTIC_TOKEN };
|
|
7537
7607
|
export { compareSemver, githubRepoSlug, filterReleasesSince, renderReleaseBody };
|
|
7538
7608
|
export {
|
|
7539
7609
|
webConsoleUrl,
|
|
@@ -7624,6 +7694,7 @@ export {
|
|
|
7624
7694
|
formatDuration,
|
|
7625
7695
|
summarizeSupervisorWorker,
|
|
7626
7696
|
formatSupervisorStatus,
|
|
7697
|
+
printSupervisorStatus,
|
|
7627
7698
|
supervisorStatusSignature,
|
|
7628
7699
|
supervisorJobCell,
|
|
7629
7700
|
supervisorWorkerActivityFile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.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.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"
|
|
67
67
|
}
|
|
68
68
|
}
|