c8ctl-plugin-nano 1.38.1 → 1.39.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/c8ctl-plugin.js +227 -135
- package/package.json +9 -9
package/c8ctl-plugin.js
CHANGED
|
@@ -2458,18 +2458,12 @@ function resolveBrokerRestConfig(env = process.env, opts = {}) {
|
|
|
2458
2458
|
// exposes no usable restAddress. The token same-origin gate lives in
|
|
2459
2459
|
// resolveBrokerRestConfig (re-run against the profile base), never duplicated.
|
|
2460
2460
|
function resolveAutoRestConfig(camunda, env = process.env) {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
} catch {
|
|
2468
|
-
// ignore — degrade to the resolveBrokerRestConfig (localhost) default below
|
|
2469
|
-
}
|
|
2470
|
-
if (profileBase) return resolveBrokerRestConfig(env, { baseUrl: profileBase });
|
|
2471
|
-
}
|
|
2472
|
-
return resolveBrokerRestConfig(env);
|
|
2461
|
+
// Derive the base from the single canonical resolver (explicit override →
|
|
2462
|
+
// profile restAddress → localhost), then run it through resolveBrokerRestConfig
|
|
2463
|
+
// so the token same-origin gate stays single-sourced. resolveWorkerEngineBase
|
|
2464
|
+
// always returns a base, so opts.baseUrl always pins it here.
|
|
2465
|
+
const baseUrl = resolveWorkerEngineBase(camunda, env);
|
|
2466
|
+
return resolveBrokerRestConfig(env, { baseUrl });
|
|
2473
2467
|
}
|
|
2474
2468
|
|
|
2475
2469
|
// ---------------------------------------------------------------------------
|
|
@@ -2483,82 +2477,43 @@ function resolveAutoRestConfig(camunda, env = process.env) {
|
|
|
2483
2477
|
// exist" is answerable from that engine alone.
|
|
2484
2478
|
//
|
|
2485
2479
|
// `@nanobpm/agentic/demand` already reads deployed `taskDefinition` leaves over
|
|
2486
|
-
// C8 REST (`process-definitions/search` → `/{key}/xml`)
|
|
2487
|
-
//
|
|
2488
|
-
//
|
|
2489
|
-
//
|
|
2490
|
-
//
|
|
2491
|
-
//
|
|
2492
|
-
//
|
|
2493
|
-
//
|
|
2480
|
+
// C8 REST (`process-definitions/search` → `/{key}/xml`). As of
|
|
2481
|
+
// `@nanobpm/agentic@0.4.0` its `scanTaskDefinitions(xml)` tags every leaf with a
|
|
2482
|
+
// canonical `agentic: boolean` — true iff the service task declares a
|
|
2483
|
+
// `<zeebe:linkedResource … linkName="prompt">` base-prompt side-car (its internal
|
|
2484
|
+
// `hasPromptLink`). That flag is the SINGLE SOURCE OF TRUTH for agentic-ness (see
|
|
2485
|
+
// the package's `demand/taskdef.d.ts` and nano-workforce SPEC "Agent job
|
|
2486
|
+
// contract"): every external agent task delivers its base prompt through a
|
|
2487
|
+
// `linkName="prompt"` linked resource, and no in-process worker task does. Not
|
|
2488
|
+
// every service task is an agent task — plain connectors and record-keepers
|
|
2489
|
+
// (e.g. `pr.record-plan`) are ordinary workers, and they carry no prompt link.
|
|
2490
|
+
//
|
|
2491
|
+
// Per AGENTS.md "Derivation Over Duplication: No Drift Surfaces", this plugin
|
|
2492
|
+
// CONSUMES that flag rather than re-implementing the scan, so the detector can
|
|
2493
|
+
// never drift out of lock-step with the package again (as it did in #95, when a
|
|
2494
|
+
// local copy keyed on the legacy `io.nanobpm.agentTask` header missed the current
|
|
2495
|
+
// linked-prompt marker). Advertise the raw job-type string the engine matches
|
|
2496
|
+
// (`senior:plan`) verbatim — colon-named types are NOT forced through the agentic
|
|
2497
|
+
// dot-grammar.
|
|
2494
2498
|
// ---------------------------------------------------------------------------
|
|
2495
2499
|
|
|
2496
|
-
//
|
|
2497
|
-
// `
|
|
2498
|
-
//
|
|
2499
|
-
// connector / record-keeper. Matches the exact `io.nanobpm.agentTask` key and
|
|
2500
|
-
// any flattened `io.nanobpm.agentTask.*` dotpath key (element templates emit the
|
|
2501
|
-
// latter, e.g. `io.nanobpm.agentTask.task.prompt`). Older deployments carry this;
|
|
2502
|
-
// the current `@nanobpm/workflow` toolchain emits the linked-prompt marker below
|
|
2503
|
-
// instead, so BOTH must be recognised (jwulf/c8ctl-plugin-nano#95).
|
|
2504
|
-
function serviceTaskHasAgentHeader(body) {
|
|
2505
|
-
const headerRe = /<zeebe:header\b[^>]*\bkey\s*=\s*"([^"]*)"/g;
|
|
2506
|
-
let m;
|
|
2507
|
-
while ((m = headerRe.exec(body)) !== null) {
|
|
2508
|
-
const key = m[1];
|
|
2509
|
-
if (key === AGENT_TASK_NS || key.startsWith(`${AGENT_TASK_NS}.`)) return true;
|
|
2510
|
-
}
|
|
2511
|
-
return false;
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
// True iff a serviceTask body links a prompt resource — the CURRENT canonical
|
|
2515
|
-
// agent-task marker emitted by `@nanobpm/workflow` / the Urban toolchain:
|
|
2516
|
-
// `<zeebe:linkedResource … resourceType="GenericScript" linkName="prompt" />`.
|
|
2517
|
-
// An agent task links the model/prompt the harness runs; a plain connector /
|
|
2518
|
-
// record-keeper does not. Matched by the `linkName="prompt"` binding (attribute
|
|
2519
|
-
// order-independent) so a compiled model with no `io.nanobpm.agentTask` header
|
|
2520
|
-
// is still discovered (jwulf/c8ctl-plugin-nano#95). Kept in lock-step with the
|
|
2521
|
-
// authored nano-workforce models (resources/processes/*.bpmn), where every
|
|
2522
|
-
// `senior:*` service task carries this binding and none carry the legacy header.
|
|
2523
|
-
// NOTE: this mirrors `@nanobpm/agentic@0.4.0`'s released `scanTaskDefinitions`
|
|
2524
|
-
// `agentic` flag (its internal `hasPromptLink`); #102 tracks replacing this local
|
|
2525
|
-
// copy by consuming that detector once the `^0.1.0 → ^0.4.0` bump is vetted.
|
|
2526
|
-
function serviceTaskHasLinkedPrompt(body) {
|
|
2527
|
-
const re = new RegExp(`<zeebe:linkedResource\\b[^>]*\\blinkName\\s*=\\s*"${DEFAULT_PROMPT_LINK_NAME}"`);
|
|
2528
|
-
return re.test(String(body || ''));
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
// True iff a serviceTask is an *agent* task — by EITHER the legacy
|
|
2532
|
-
// `io.nanobpm.agentTask.*` header OR the current linked-prompt marker. Either
|
|
2533
|
-
// alone is sufficient; deployments in the wild carry one or the other.
|
|
2534
|
-
function serviceTaskIsAgentTask(body) {
|
|
2535
|
-
return serviceTaskHasAgentHeader(body) || serviceTaskHasLinkedPrompt(body);
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
// Scan one deployed BPMN document for its *agent* task-definition leaves: every
|
|
2539
|
-
// `<bpmn:serviceTask>` carrying BOTH a non-empty `<zeebe:taskDefinition type>`
|
|
2540
|
-
// AND an agent-task marker (legacy `io.nanobpm.agentTask.` header OR a
|
|
2500
|
+
// Scan one deployed BPMN document for its *agent* task-definition leaves: the
|
|
2501
|
+
// subset of `@nanobpm/agentic` `demand.scanTaskDefinitions(xml)` leaves whose
|
|
2502
|
+
// canonical `agentic` flag is set (i.e. the service task declares a
|
|
2541
2503
|
// `linkName="prompt"` linked resource). Returns `{ taskType, process }` leaves in
|
|
2542
|
-
// first-occurrence order.
|
|
2543
|
-
//
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
while ((block = blockRe.exec(source)) !== null) {
|
|
2552
|
-
const body = block[1];
|
|
2553
|
-
const tdMatch = body.match(/<zeebe:taskDefinition\b[^>]*>/);
|
|
2554
|
-
if (!tdMatch) continue;
|
|
2555
|
-
const typeMatch = tdMatch[0].match(/\btype\s*=\s*"([^"]*)"/);
|
|
2556
|
-
const taskType = typeMatch ? typeMatch[1] : '';
|
|
2557
|
-
if (!taskType) continue;
|
|
2558
|
-
if (!serviceTaskIsAgentTask(body)) continue;
|
|
2559
|
-
out.push({ taskType, process: proc });
|
|
2504
|
+
// first-occurrence order. The published `scanTaskDefinitions` is INJECTED so this
|
|
2505
|
+
// stays a pure, synchronous function; `readDeployedAgentJobTypes` supplies the
|
|
2506
|
+
// real one from the lazily-imported demand surface (`agentic.mjs`).
|
|
2507
|
+
function scanAgentTaskLeaves(xml, scanTaskDefinitions) {
|
|
2508
|
+
if (typeof scanTaskDefinitions !== 'function') {
|
|
2509
|
+
throw new TypeError(
|
|
2510
|
+
'scanAgentTaskLeaves: `scanTaskDefinitions` must be an injected function ' +
|
|
2511
|
+
`(got ${typeof scanTaskDefinitions}); pass demand.scanTaskDefinitions from ./agentic.mjs`
|
|
2512
|
+
);
|
|
2560
2513
|
}
|
|
2561
|
-
return
|
|
2514
|
+
return scanTaskDefinitions(String(xml || ''))
|
|
2515
|
+
.filter((leaf) => leaf.agentic)
|
|
2516
|
+
.map((leaf) => ({ taskType: leaf.taskType, process: leaf.process }));
|
|
2562
2517
|
}
|
|
2563
2518
|
|
|
2564
2519
|
// Read the distinct deployed *agent* job types through a demand C8RestReader
|
|
@@ -2567,12 +2522,13 @@ function scanAgentTaskLeaves(xml) {
|
|
|
2567
2522
|
// optional `scope` narrows to one app/network — kept only when the leaf's
|
|
2568
2523
|
// `bpmn:process` id equals or is prefixed by the scope string.
|
|
2569
2524
|
async function readDeployedAgentJobTypes(reader, { scope = '' } = {}) {
|
|
2525
|
+
const { demand } = await import('./agentic.mjs');
|
|
2570
2526
|
const keys = await reader.searchProcessDefinitionKeys();
|
|
2571
2527
|
const seen = new Set();
|
|
2572
2528
|
const out = [];
|
|
2573
2529
|
for (const key of keys) {
|
|
2574
2530
|
const xml = await reader.getProcessDefinitionXml(key);
|
|
2575
|
-
for (const leaf of scanAgentTaskLeaves(xml)) {
|
|
2531
|
+
for (const leaf of scanAgentTaskLeaves(xml, demand.scanTaskDefinitions)) {
|
|
2576
2532
|
if (scope && !(leaf.process === scope || leaf.process.startsWith(scope))) continue;
|
|
2577
2533
|
if (seen.has(leaf.taskType)) continue;
|
|
2578
2534
|
seen.add(leaf.taskType);
|
|
@@ -2685,28 +2641,117 @@ function normalizeRestBase(addr) {
|
|
|
2685
2641
|
return String(addr || '').replace(/\/+$/, '').replace(/\/v2$/i, '');
|
|
2686
2642
|
}
|
|
2687
2643
|
|
|
2644
|
+
// The ONE place that answers "what engine base does this worker use?" — the
|
|
2645
|
+
// single source of truth for the worker's engine-base precedence chain:
|
|
2646
|
+
//
|
|
2647
|
+
// explicit override (NANO_REST_URL → NANO_BASE_URL → persisted cfg.nanoUrl)
|
|
2648
|
+
// → active c8ctl profile restAddress (the client that activates jobs)
|
|
2649
|
+
// → localhost default (DEFAULT_NANO_URL).
|
|
2650
|
+
//
|
|
2651
|
+
// Every worker engine reference derives from this (derivation over duplication):
|
|
2652
|
+
// the `--auto` job-type read (resolveAutoRestConfig, injecting it into the
|
|
2653
|
+
// resolveBrokerRestConfig token gate), the linked-prompt fetch
|
|
2654
|
+
// (resolveLinkedPromptSource), the `supervisor status` engine column
|
|
2655
|
+
// (workerEngine), and the agentic visibility channel base (resolveAgenticConfig).
|
|
2656
|
+
// Before this existed the chain was hand-copied at each site and the agentic copy
|
|
2657
|
+
// silently lost the profile fallback, so a profile-only remote worker degraded to
|
|
2658
|
+
// its own localhost and never enrolled (jwulf/c8ctl-plugin-nano#107, sibling of
|
|
2659
|
+
// #93/#99). Sharing this resolver fixes that by construction and removes the
|
|
2660
|
+
// drift surface. The token same-origin gate stays in resolveBrokerRestConfig,
|
|
2661
|
+
// never duplicated here.
|
|
2662
|
+
function resolveWorkerEngineBase(camunda, env = process.env) {
|
|
2663
|
+
// readConfig() swallows parse/IO errors and returns {} — never throws.
|
|
2664
|
+
const cfg = readConfig() || {};
|
|
2665
|
+
const explicit = env.NANO_REST_URL || env.NANO_BASE_URL || cfg.nanoUrl;
|
|
2666
|
+
if (explicit) return normalizeRestBase(explicit);
|
|
2667
|
+
if (camunda && typeof camunda.getConfig === 'function') {
|
|
2668
|
+
try {
|
|
2669
|
+
const profileBase = normalizeRestBase(camunda.getConfig()?.restAddress);
|
|
2670
|
+
if (profileBase) return profileBase;
|
|
2671
|
+
} catch {
|
|
2672
|
+
// degrade to the localhost default below rather than throw
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return DEFAULT_NANO_URL;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// The engine authority reported in the supervisor activity marker's ENGINE
|
|
2679
|
+
// column (#99): the base the worker actually POLLS JOBS from. The SDK job worker
|
|
2680
|
+
// (`camunda.createJobWorker`) activates jobs against the client's OWN profile
|
|
2681
|
+
// restAddress — which is NOT affected by the explicit NANO_REST_URL / NANO_BASE_URL
|
|
2682
|
+
// / cfg.nanoUrl overrides that resolveWorkerEngineBase prefers for auxiliary REST
|
|
2683
|
+
// reads (linked prompts, `--auto` reads, agentic channel). Reporting an override
|
|
2684
|
+
// there would make the ENGINE column claim an engine the worker is not polling,
|
|
2685
|
+
// violating supervisorEngineCell's "engine this worker polls jobs from" contract.
|
|
2686
|
+
// So prefer the profile restAddress (the polling engine) and fall back to the
|
|
2687
|
+
// canonical resolver only when the client exposes no usable base.
|
|
2688
|
+
function resolveWorkerPollEngineBase(camunda, env = process.env) {
|
|
2689
|
+
if (camunda && typeof camunda.getConfig === 'function') {
|
|
2690
|
+
try {
|
|
2691
|
+
const profileBase = normalizeRestBase(camunda.getConfig()?.restAddress);
|
|
2692
|
+
if (profileBase) return profileBase;
|
|
2693
|
+
} catch {
|
|
2694
|
+
// degrade to the canonical resolver below rather than throw
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return resolveWorkerEngineBase(camunda, env);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
// The base URL for a linked-prompt fetch. A linked-resource `resourceKey` is
|
|
2701
|
+
// BROKER-LOCAL: it lives on the engine the SDK client activated this job against
|
|
2702
|
+
// — the polling engine (its OWN profile restAddress). So the prompt fetch must
|
|
2703
|
+
// target that same broker, NOT the NANO_BASE_URL / cfg.nanoUrl auxiliary-read
|
|
2704
|
+
// overrides that resolveWorkerEngineBase prefers. Those overrides steer reads
|
|
2705
|
+
// that are NOT broker-local; honoring them here makes the base drift from the
|
|
2706
|
+
// activation broker so a broker-local resourceKey 404s ("prompt resource N fetch
|
|
2707
|
+
// failed") even while job activation still succeeds. Precedence:
|
|
2708
|
+
//
|
|
2709
|
+
// explicit NANO_REST_URL (same-broker escape hatch, e.g. a caching proxy in
|
|
2710
|
+
// front of the polling engine)
|
|
2711
|
+
// → polling engine base (resolveWorkerPollEngineBase → profile restAddress)
|
|
2712
|
+
// → localhost default.
|
|
2713
|
+
//
|
|
2714
|
+
// NANO_REST_URL is retained as an explicit escape hatch, but NANO_BASE_URL /
|
|
2715
|
+
// cfg.nanoUrl are deliberately NOT — steering the prompt fetch to a different
|
|
2716
|
+
// engine than the one that issued the broker-local resourceKey is the very bug
|
|
2717
|
+
// this resolves.
|
|
2718
|
+
function resolveLinkedPromptBase(camunda, env = process.env) {
|
|
2719
|
+
const explicit = env.NANO_REST_URL;
|
|
2720
|
+
if (explicit) return normalizeRestBase(explicit);
|
|
2721
|
+
return resolveWorkerPollEngineBase(camunda, env);
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2688
2724
|
// Derive the linked-resource fetch base URL + auth headers from the SAME SDK
|
|
2689
2725
|
// client that activated the job. A linked-resource `resourceKey` is broker-local,
|
|
2690
|
-
// so prompt content must be fetched from the broker this worker
|
|
2691
|
-
//
|
|
2692
|
-
//
|
|
2693
|
-
//
|
|
2694
|
-
//
|
|
2726
|
+
// so prompt content must be fetched from the broker this worker POLLS jobs from
|
|
2727
|
+
// (the polling engine — its profile restAddress). The base comes from
|
|
2728
|
+
// resolveLinkedPromptBase (explicit NANO_REST_URL escape hatch → polling engine
|
|
2729
|
+
// base → localhost); it deliberately does NOT follow the NANO_BASE_URL /
|
|
2730
|
+
// cfg.nanoUrl auxiliary-read overrides, since those would point the fetch at an
|
|
2731
|
+
// engine that never issued this broker-local resourceKey (the cause of "prompt
|
|
2732
|
+
// resource N fetch failed"). getAuthHeaders() is guarded so an older or atypical
|
|
2733
|
+
// client runtime degrades to the legacy path rather than throw.
|
|
2734
|
+
//
|
|
2735
|
+
// The base URL is invariant for a worker's lifetime, so callers on the per-job
|
|
2736
|
+
// hot path pass the once-at-startup `resolveLinkedPromptBase` result via
|
|
2737
|
+
// `baseUrl` to skip the synchronous config.json read (existsSync + readFileSync)
|
|
2738
|
+
// that this resolver would otherwise repeat on every job; only the auth headers
|
|
2739
|
+
// (which the client may rotate) are resolved per call. When `baseUrl` is omitted
|
|
2740
|
+
// it falls back to computing the base itself, so standalone callers and tests
|
|
2741
|
+
// keep the single-argument behaviour.
|
|
2695
2742
|
//
|
|
2696
2743
|
// TODO: once c8ctl bumps @camunda8/orchestration-cluster-api to v10 (10.0.0-alpha
|
|
2697
2744
|
// exposes the typed camunda.getResourceContentBinary({resourceKey}) → Blob), drop
|
|
2698
2745
|
// this raw /content/binary fetch and call that method directly. The pinned ^9.1.0
|
|
2699
2746
|
// SDK only exposes the deprecated getResourceContent, which 406s for generic
|
|
2700
2747
|
// (Markdown) prompt resources — see camunda/orchestration-cluster-api-js.
|
|
2701
|
-
async function resolveLinkedPromptSource(camunda, env = process.env) {
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2748
|
+
async function resolveLinkedPromptSource(camunda, env = process.env, { baseUrl: preResolvedBase } = {}) {
|
|
2749
|
+
// The fetch base follows resolveLinkedPromptBase (explicit NANO_REST_URL escape
|
|
2750
|
+
// hatch → polling engine base → localhost) so it tracks the broker the job was
|
|
2751
|
+
// activated against — the resourceKey is broker-local. A caller-supplied
|
|
2752
|
+
// pre-resolved base (already normalized at worker startup) is reused verbatim to
|
|
2753
|
+
// avoid a per-job config.json read.
|
|
2754
|
+
const baseUrl = preResolvedBase ?? resolveLinkedPromptBase(camunda, env);
|
|
2710
2755
|
let authHeaders;
|
|
2711
2756
|
if (env.NANO_REST_TOKEN) {
|
|
2712
2757
|
authHeaders = { Authorization: `Bearer ${env.NANO_REST_TOKEN}` };
|
|
@@ -4121,13 +4166,19 @@ function buildResultEnvelope(result, { sandbox, image, git, result: agentResult,
|
|
|
4121
4166
|
* secret is ignored (LOCAL mode).
|
|
4122
4167
|
* - OFF: NANO_AGENTIC=off (or 0/false/no), or persisted `agentic:false`.
|
|
4123
4168
|
*
|
|
4124
|
-
* Env wins over persisted config;
|
|
4125
|
-
* URL
|
|
4169
|
+
* Env wins over persisted config; when no explicit agentic target is set the base
|
|
4170
|
+
* URL defers to the shared resolveWorkerEngineBase (explicit engine override →
|
|
4171
|
+
* active c8ctl profile restAddress → localhost default), so a profile-only remote
|
|
4172
|
+
* worker discovers the agentic channel of the engine its jobs actually run on
|
|
4173
|
+
* rather than its own loopback (jwulf/c8ctl-plugin-nano#107). It is never empty.
|
|
4126
4174
|
* Returns `null` only when disabled (the off-switch).
|
|
4127
4175
|
*
|
|
4176
|
+
* @param {object} [camunda] the SDK client that activates jobs — its
|
|
4177
|
+
* getConfig().restAddress supplies the profile engine base when no explicit
|
|
4178
|
+
* agentic/engine override is set. Threaded from the `work` call site.
|
|
4128
4179
|
* @returns {{ url: string, token: string, credential: string, bufferCapacity: number, secure: boolean, explicitUrl: boolean } | null}
|
|
4129
4180
|
*/
|
|
4130
|
-
function resolveAgenticConfig() {
|
|
4181
|
+
function resolveAgenticConfig(camunda) {
|
|
4131
4182
|
const cfg = readConfig();
|
|
4132
4183
|
// Explicit off-switch (env wins). Lets an operator fully opt out of visibility.
|
|
4133
4184
|
const offSetting = process.env.NANO_AGENTIC
|
|
@@ -4136,14 +4187,14 @@ function resolveAgenticConfig() {
|
|
|
4136
4187
|
|
|
4137
4188
|
// An explicit agentic target (env NANO_AGENTIC_URL or persisted `agenticUrl`)
|
|
4138
4189
|
// is used verbatim and short-circuits hub auto-discovery (#75). When neither is
|
|
4139
|
-
// set the
|
|
4140
|
-
//
|
|
4190
|
+
// set the base defers to the shared worker-engine resolver (explicit engine
|
|
4191
|
+
// override → profile restAddress → localhost default) — NOT a re-inlined
|
|
4192
|
+
// nanoUrl → NANO_BASE_URL → default chain — so agentic discovery targets the
|
|
4193
|
+
// same engine the worker's jobs run on (jwulf/c8ctl-plugin-nano#107).
|
|
4141
4194
|
const explicitUrl = !!(process.env.NANO_AGENTIC_URL || cfg.agenticUrl);
|
|
4142
4195
|
const url = process.env.NANO_AGENTIC_URL
|
|
4143
4196
|
|| cfg.agenticUrl
|
|
4144
|
-
||
|
|
4145
|
-
|| process.env.NANO_BASE_URL
|
|
4146
|
-
|| DEFAULT_NANO_URL;
|
|
4197
|
+
|| resolveWorkerEngineBase(camunda);
|
|
4147
4198
|
// SECURE-mode shared secret. Named NANO_AGENTIC_SECRET to match the server's env
|
|
4148
4199
|
// var EXACTLY (Tab A → Slot A): set the same name + value on the server and every
|
|
4149
4200
|
// worker box. The worker presents it as its identity token; the hub verifies it
|
|
@@ -4387,11 +4438,11 @@ async function discoverAgenticHubs(engineBaseUrl, {
|
|
|
4387
4438
|
* no projects API / not a nano engine, or discovery error/timeout). The
|
|
4388
4439
|
* worker continues doing real work with no channel.
|
|
4389
4440
|
*
|
|
4390
|
-
* @param {{ fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
|
|
4441
|
+
* @param {{ camunda?: object, fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
|
|
4391
4442
|
* @returns {Promise<{ status: string, config?: object, message?: string, candidates?: Array }>}
|
|
4392
4443
|
*/
|
|
4393
|
-
async function resolveAgenticTarget(opts = {}) {
|
|
4394
|
-
const base = resolveAgenticConfig();
|
|
4444
|
+
async function resolveAgenticTarget({ camunda, ...opts } = {}) {
|
|
4445
|
+
const base = resolveAgenticConfig(camunda);
|
|
4395
4446
|
if (!base) return { status: 'off' };
|
|
4396
4447
|
// Explicit target wins verbatim and skips discovery entirely.
|
|
4397
4448
|
if (base.explicitUrl) return { status: 'connect', config: base };
|
|
@@ -4730,7 +4781,9 @@ async function workAgent(req, flags) {
|
|
|
4730
4781
|
// no capability, no app enrol endpoint, no channel connection. It is the
|
|
4731
4782
|
// mutually-exclusive counterpart to capability-resolved SERVE: in `--auto`
|
|
4732
4783
|
// the rank×capability matrix is bypassed entirely (any deployed agent job is
|
|
4733
|
-
// served, gated only by the `
|
|
4784
|
+
// served, gated only by the leaf's canonical `agentic` flag — the
|
|
4785
|
+
// `linkName="prompt"` linked-resource marker read by
|
|
4786
|
+
// `@nanobpm/agentic`'s demand scanner), and the
|
|
4734
4787
|
// desired set is reconciled by polling the engine rather than watching the
|
|
4735
4788
|
// profile. `--auto-scope <process-id|prefix>` narrows the blast radius to one
|
|
4736
4789
|
// app/network; without it, every agent job type on the engine is served.
|
|
@@ -4810,24 +4863,30 @@ async function workAgent(req, flags) {
|
|
|
4810
4863
|
installParentDeathWatchdog({ parentPid: Number.isInteger(daemonPid) ? daemonPid : undefined });
|
|
4811
4864
|
}
|
|
4812
4865
|
const activeJobs = new Map(); // jobKey -> { type, since (ms epoch) }
|
|
4813
|
-
// Which engine this worker polls jobs from
|
|
4814
|
-
//
|
|
4815
|
-
//
|
|
4816
|
-
//
|
|
4817
|
-
//
|
|
4818
|
-
//
|
|
4819
|
-
//
|
|
4820
|
-
//
|
|
4821
|
-
|
|
4822
|
-
//
|
|
4823
|
-
//
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4866
|
+
// Which engine this worker polls jobs from, surfaced to `supervisor status` via
|
|
4867
|
+
// the activity marker (#99). Derived from resolveWorkerPollEngineBase — the SDK
|
|
4868
|
+
// client's OWN profile restAddress (the base createJobWorker actually activates
|
|
4869
|
+
// against), NOT the NANO_* / cfg.nanoUrl override that resolveWorkerEngineBase
|
|
4870
|
+
// prefers for auxiliary REST reads. That keeps the ENGINE column honest: it
|
|
4871
|
+
// reports the engine the worker truly polls, never an override the job worker
|
|
4872
|
+
// ignores. Falls back to the canonical resolver only when the client exposes no
|
|
4873
|
+
// usable profile base.
|
|
4874
|
+
const workerEngine = resolveWorkerPollEngineBase(camunda);
|
|
4875
|
+
// Base URL for the per-job linked-prompt fetch (issue #63). This follows
|
|
4876
|
+
// resolveLinkedPromptBase — the polling engine (profile restAddress the SDK
|
|
4877
|
+
// activates jobs against), since the linked resourceKey is BROKER-LOCAL and must
|
|
4878
|
+
// be fetched from the broker that issued it — with NANO_REST_URL as the only
|
|
4879
|
+
// explicit escape hatch. It deliberately does NOT follow the NANO_BASE_URL /
|
|
4880
|
+
// cfg.nanoUrl auxiliary-read overrides resolveWorkerEngineBase prefers: pointing
|
|
4881
|
+
// the prompt fetch at a different engine than the activation broker would 404 a
|
|
4882
|
+
// broker-local resourceKey while job activation still succeeds. Computed ONCE at
|
|
4883
|
+
// startup so the per-job hot path skips a config.json read. It coincides with
|
|
4884
|
+
// `workerEngine` (the polling engine) unless NANO_REST_URL is set.
|
|
4885
|
+
const linkedPromptBase = resolveLinkedPromptBase(camunda);
|
|
4886
|
+
// The live agentic-visibility channel status, also surfaced to `supervisor
|
|
4887
|
+
// status` via the activity marker (#99). `agenticState` starts 'starting' and
|
|
4888
|
+
// is updated once the channel target is resolved and again on each
|
|
4889
|
+
// connect/disconnect below.
|
|
4831
4890
|
let agenticState = { status: 'starting' };
|
|
4832
4891
|
const writeActivity = () => {
|
|
4833
4892
|
if (!activityFile) return;
|
|
@@ -4879,7 +4938,7 @@ async function workAgent(req, flags) {
|
|
|
4879
4938
|
// worker joins with the well-known LOCAL token and no credential; SECURE mode
|
|
4880
4939
|
// (NANO_AGENTIC_SECRET) sends a real per-peer shared secret as the identity;
|
|
4881
4940
|
// NANO_AGENTIC=off disables it (see resolveAgenticConfig).
|
|
4882
|
-
const agenticTarget = await resolveAgenticTarget({ logger });
|
|
4941
|
+
const agenticTarget = await resolveAgenticTarget({ camunda, logger });
|
|
4883
4942
|
let agenticCfg = null;
|
|
4884
4943
|
// buildAgenticUrl can throw on a malformed/unsupported explicit NANO_AGENTIC_URL.
|
|
4885
4944
|
// This is only the display URL for the activity marker, so compute it
|
|
@@ -5048,8 +5107,10 @@ async function workAgent(req, flags) {
|
|
|
5048
5107
|
try {
|
|
5049
5108
|
// Fetch the prompt from the broker the SDK client is connected to,
|
|
5050
5109
|
// deriving base URL + auth from that client (not restConfig, whose base
|
|
5051
|
-
// defaults to localhost) — the resourceKey is broker-local.
|
|
5052
|
-
|
|
5110
|
+
// defaults to localhost) — the resourceKey is broker-local. The base is
|
|
5111
|
+
// invariant, so reuse the once-at-startup linkedPromptBase and let the
|
|
5112
|
+
// resolver only compute per-job auth headers (no per-job config.json read).
|
|
5113
|
+
const promptSource = await resolveLinkedPromptSource(camunda, process.env, { baseUrl: linkedPromptBase });
|
|
5053
5114
|
const linked = await resolveLinkedPrompt(job.customHeaders ?? {}, {
|
|
5054
5115
|
baseUrl: promptSource.baseUrl || restConfig.baseUrl,
|
|
5055
5116
|
authHeaders: promptSource.authHeaders,
|
|
@@ -5864,6 +5925,10 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
|
|
|
5864
5925
|
startedAtMs,
|
|
5865
5926
|
lastExit: w.lastExit ?? null,
|
|
5866
5927
|
args: Array.isArray(w.args) ? w.args : [],
|
|
5928
|
+
// The per-worker log path (logs/supervisor/worker-<id>.log). Carried through
|
|
5929
|
+
// so `supervisor status` can surface it (see formatSupervisorLogsLines);
|
|
5930
|
+
// null when the source record predates it (e.g. an older persisted state).
|
|
5931
|
+
logFile: w.logFile ?? null,
|
|
5867
5932
|
activity,
|
|
5868
5933
|
engine,
|
|
5869
5934
|
agentic,
|
|
@@ -6070,6 +6135,30 @@ function createSupervisorLiveView({
|
|
|
6070
6135
|
};
|
|
6071
6136
|
}
|
|
6072
6137
|
|
|
6138
|
+
/**
|
|
6139
|
+
* The `Logs:` block for `supervisor status`, derived purely from the status
|
|
6140
|
+
* payload so it renders identically for a live daemon status and a synthesized
|
|
6141
|
+
* one. Lists the daemon log and each worker's log file, plus a hint at the
|
|
6142
|
+
* existing tailer. Returns an empty array (no section) when no log path is
|
|
6143
|
+
* known — an older persisted state, or a dead daemon whose frame carried none.
|
|
6144
|
+
*/
|
|
6145
|
+
function formatSupervisorLogsLines(status) {
|
|
6146
|
+
const daemonLog = status?.daemon?.logFile || null;
|
|
6147
|
+
const workers = Array.isArray(status?.workers) ? status.workers : [];
|
|
6148
|
+
const workerLogs = workers
|
|
6149
|
+
.filter((w) => w && w.logFile)
|
|
6150
|
+
.map((w) => ({ label: String(w.id), path: String(w.logFile) }));
|
|
6151
|
+
const entries = [];
|
|
6152
|
+
if (daemonLog) entries.push({ label: 'daemon', path: String(daemonLog) });
|
|
6153
|
+
for (const w of workerLogs) entries.push(w);
|
|
6154
|
+
if (entries.length === 0) return [];
|
|
6155
|
+
const labelWidth = Math.max(...entries.map((e) => e.label.length));
|
|
6156
|
+
const out = ['', 'Logs:'];
|
|
6157
|
+
for (const e of entries) out.push(` ${e.label.padEnd(labelWidth)} ${e.path}`);
|
|
6158
|
+
out.push(' View: c8ctl nano supervisor logs [<id>] [--follow]');
|
|
6159
|
+
return out;
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6073
6162
|
/** Render a supervisor status object as an aligned text table. */
|
|
6074
6163
|
function formatSupervisorStatus(status) {
|
|
6075
6164
|
const lines = [];
|
|
@@ -6083,6 +6172,7 @@ function formatSupervisorStatus(status) {
|
|
|
6083
6172
|
lines.push('');
|
|
6084
6173
|
if (workers.length === 0) {
|
|
6085
6174
|
lines.push(' No workers. Add one with: c8ctl nano supervisor add <profile>');
|
|
6175
|
+
lines.push(...formatSupervisorLogsLines(status));
|
|
6086
6176
|
return lines.join('\n');
|
|
6087
6177
|
}
|
|
6088
6178
|
const rows = workers.map((w) => ({
|
|
@@ -6107,6 +6197,7 @@ function formatSupervisorStatus(status) {
|
|
|
6107
6197
|
const fmt = (r) => ' ' + cols.map((c) => r[c].padEnd(width[c])).join(' ');
|
|
6108
6198
|
lines.push(fmt(head));
|
|
6109
6199
|
for (const r of rows) lines.push(fmt(r));
|
|
6200
|
+
lines.push(...formatSupervisorLogsLines(status));
|
|
6110
6201
|
return lines.join('\n');
|
|
6111
6202
|
}
|
|
6112
6203
|
|
|
@@ -8858,6 +8949,9 @@ export {
|
|
|
8858
8949
|
pickLinkedResource,
|
|
8859
8950
|
resolveBrokerRestConfig,
|
|
8860
8951
|
resolveAutoRestConfig,
|
|
8952
|
+
resolveWorkerEngineBase,
|
|
8953
|
+
resolveWorkerPollEngineBase,
|
|
8954
|
+
resolveLinkedPromptBase,
|
|
8861
8955
|
resourceContentUrl,
|
|
8862
8956
|
fetchLinkedResourceContent,
|
|
8863
8957
|
resolveLinkedPrompt,
|
|
@@ -8909,9 +9003,6 @@ export {
|
|
|
8909
9003
|
jobTypeMatrix,
|
|
8910
9004
|
diffJobTypes,
|
|
8911
9005
|
parseJobTypeFlags,
|
|
8912
|
-
serviceTaskHasAgentHeader,
|
|
8913
|
-
serviceTaskHasLinkedPrompt,
|
|
8914
|
-
serviceTaskIsAgentTask,
|
|
8915
9006
|
scanAgentTaskLeaves,
|
|
8916
9007
|
readDeployedAgentJobTypes,
|
|
8917
9008
|
resolveAutoJobTypes,
|
|
@@ -8942,6 +9033,7 @@ export {
|
|
|
8942
9033
|
formatDuration,
|
|
8943
9034
|
summarizeSupervisorWorker,
|
|
8944
9035
|
formatSupervisorStatus,
|
|
9036
|
+
formatSupervisorLogsLines,
|
|
8945
9037
|
reageSupervisorStatus,
|
|
8946
9038
|
clampToWidth,
|
|
8947
9039
|
createSupervisorLiveView,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.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",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"semantic-release": "^25.0.3"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@nanobpm/agentic": "^0.
|
|
55
|
+
"@nanobpm/agentic": "^0.4.0",
|
|
56
56
|
"@nanobpm/urban-agent-client": "^0.1.4"
|
|
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.39.1",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.39.1",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.39.1",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.39.1",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.39.1",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.39.1",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.39.1"
|
|
67
67
|
}
|
|
68
68
|
}
|