c8ctl-plugin-nano 1.39.0 → 1.39.2

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 CHANGED
@@ -208,7 +208,7 @@ c8ctl nano work reviewer --job-type senior:pr-review --job-type senior:triage
208
208
 
209
209
  ```bash
210
210
  c8ctl nano work reviewer # poll for work until Ctrl-C
211
- c8ctl nano work reviewer --max-parallel 2 --recovery-window 300000
211
+ c8ctl nano work reviewer --recovery-window 300000
212
212
  c8ctl nano work reviewer --name reviewer-eu # name this worker (else auto ‹host›-‹profile›-‹random›)
213
213
  ```
214
214
 
@@ -728,7 +728,7 @@ c8ctl nano supervisor
728
728
 
729
729
  # Manage the fleet without the console (any terminal, any time):
730
730
  c8ctl nano supervisor status # id, state, pid, restarts, uptime
731
- c8ctl nano supervisor add reviewer --max-parallel 2 # add + spawn a worker (forwards work flags)
731
+ c8ctl nano supervisor add reviewer # add + spawn a worker (forwards work flags)
732
732
  c8ctl nano supervisor add reviewer --name reviewer-2 # a SECOND reviewer, named so it stays distinct
733
733
  c8ctl nano supervisor add reviewer --instances 3 # add 3 distinct auto-named reviewers in one call
734
734
  c8ctl nano supervisor restart reviewer # by worker id or profile name
@@ -749,12 +749,13 @@ cannot be combined with `--name`; omit `--name` to let them auto-name.
749
749
  `restart`/`remove` accept either a worker id **or** a profile name — targeting a
750
750
  profile affects *every* instance of it.
751
751
 
752
- Each worker takes the **same flags as `nano work`** (`--max-parallel`,
753
- `--recovery-window`, `--idle-timeout`, `--job-timeout`, `--poll-timeout`,
752
+ Each worker takes the **same flags as `nano work`**
753
+ (`--recovery-window`, `--idle-timeout`, `--job-timeout`, `--poll-timeout`,
754
754
  `--sandbox`/`--image`, `--job-type`, `--env`, `--arg`, …); they are forwarded
755
- verbatim to the spawned child, so a supervised worker is byte-identical to a
756
- hand-run `nano work`. In the
757
- interactive console, type the flags after the profile: `add reviewer --max-parallel 2`.
755
+ to the spawned child (reconstructed via `reconstructWorkArgs`, which normalizes
756
+ ordering and coerces booleans), so a supervised worker is semantically
757
+ equivalent to a hand-run `nano work`. In the
758
+ interactive console, type the flags after the profile: `add reviewer --recovery-window 300000`.
758
759
 
759
760
  How it works and where things live:
760
761
 
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
- const cfg = readConfig() || {};
2462
- const hasExplicitBase = Boolean(env.NANO_REST_URL || env.NANO_BASE_URL || cfg.nanoUrl);
2463
- if (!hasExplicitBase && camunda && typeof camunda.getConfig === 'function') {
2464
- let profileBase = '';
2465
- try {
2466
- profileBase = normalizeRestBase(camunda.getConfig()?.restAddress);
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
  // ---------------------------------------------------------------------------
@@ -2647,28 +2641,117 @@ function normalizeRestBase(addr) {
2647
2641
  return String(addr || '').replace(/\/+$/, '').replace(/\/v2$/i, '');
2648
2642
  }
2649
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
+
2650
2724
  // Derive the linked-resource fetch base URL + auth headers from the SAME SDK
2651
2725
  // client that activated the job. A linked-resource `resourceKey` is broker-local,
2652
- // so prompt content must be fetched from the broker this worker is connected to
2653
- // never a localhost default (the cause of "prompt resource N fetch failed").
2654
- // An explicit NANO_REST_URL / NANO_REST_TOKEN override still wins as an operator
2655
- // escape hatch. Both getConfig()/getAuthHeaders() are guarded so an older or
2656
- // atypical client runtime degrades to the override/legacy path rather than throw.
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.
2657
2742
  //
2658
2743
  // TODO: once c8ctl bumps @camunda8/orchestration-cluster-api to v10 (10.0.0-alpha
2659
2744
  // exposes the typed camunda.getResourceContentBinary({resourceKey}) → Blob), drop
2660
2745
  // this raw /content/binary fetch and call that method directly. The pinned ^9.1.0
2661
2746
  // SDK only exposes the deprecated getResourceContent, which 406s for generic
2662
2747
  // (Markdown) prompt resources — see camunda/orchestration-cluster-api-js.
2663
- async function resolveLinkedPromptSource(camunda, env = process.env) {
2664
- let baseUrl = env.NANO_REST_URL || '';
2665
- if (!baseUrl && camunda && typeof camunda.getConfig === 'function') {
2666
- try {
2667
- baseUrl = normalizeRestBase(camunda.getConfig().restAddress);
2668
- } catch {
2669
- // ignore fall back to the legacy resolveBrokerRestConfig base below
2670
- }
2671
- }
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);
2672
2755
  let authHeaders;
2673
2756
  if (env.NANO_REST_TOKEN) {
2674
2757
  authHeaders = { Authorization: `Bearer ${env.NANO_REST_TOKEN}` };
@@ -2983,9 +3066,9 @@ function credArgs() {
2983
3066
  // git via GIT_ASKPASS only (never argv/URL/helper), preserving the ephemeral-token
2984
3067
  // guarantee.
2985
3068
  // Memoized for the process lifetime: this is a synchronous spawnSync (up to a
2986
- // 10s timeout) that can be reached per job, and jobs may run concurrently
2987
- // (maxParallelJobs > 1), so consult the CLI at most once per worker run rather
2988
- // than blocking every handler. A sentinel distinguishes "not yet computed" from
3069
+ // 10s timeout) that can be reached per job, so consult the CLI at most once per
3070
+ // worker run rather than blocking a handler on every job. A sentinel
3071
+ // distinguishes "not yet computed" from
2989
3072
  // a cached null (gh missing / not logged in).
2990
3073
  //
2991
3074
  // Memoization alone still lets the *first* job pay the synchronous spawn on the
@@ -4083,13 +4166,19 @@ function buildResultEnvelope(result, { sandbox, image, git, result: agentResult,
4083
4166
  * secret is ignored (LOCAL mode).
4084
4167
  * - OFF: NANO_AGENTIC=off (or 0/false/no), or persisted `agentic:false`.
4085
4168
  *
4086
- * Env wins over persisted config; the base URL falls back to the configured nano
4087
- * URL (the app's own port) and ultimately DEFAULT_NANO_URL, so it is never empty.
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.
4088
4174
  * Returns `null` only when disabled (the off-switch).
4089
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.
4090
4179
  * @returns {{ url: string, token: string, credential: string, bufferCapacity: number, secure: boolean, explicitUrl: boolean } | null}
4091
4180
  */
4092
- function resolveAgenticConfig() {
4181
+ function resolveAgenticConfig(camunda) {
4093
4182
  const cfg = readConfig();
4094
4183
  // Explicit off-switch (env wins). Lets an operator fully opt out of visibility.
4095
4184
  const offSetting = process.env.NANO_AGENTIC
@@ -4098,14 +4187,14 @@ function resolveAgenticConfig() {
4098
4187
 
4099
4188
  // An explicit agentic target (env NANO_AGENTIC_URL or persisted `agenticUrl`)
4100
4189
  // is used verbatim and short-circuits hub auto-discovery (#75). When neither is
4101
- // set the URL below is the ENGINE base (nanoUrl → NANO_BASE_URL → default), off
4102
- // which `resolveAgenticTarget()` discovers the embedded app's own /agentic port.
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).
4103
4194
  const explicitUrl = !!(process.env.NANO_AGENTIC_URL || cfg.agenticUrl);
4104
4195
  const url = process.env.NANO_AGENTIC_URL
4105
4196
  || cfg.agenticUrl
4106
- || cfg.nanoUrl
4107
- || process.env.NANO_BASE_URL
4108
- || DEFAULT_NANO_URL;
4197
+ || resolveWorkerEngineBase(camunda);
4109
4198
  // SECURE-mode shared secret. Named NANO_AGENTIC_SECRET to match the server's env
4110
4199
  // var EXACTLY (Tab A → Slot A): set the same name + value on the server and every
4111
4200
  // worker box. The worker presents it as its identity token; the hub verifies it
@@ -4349,11 +4438,11 @@ async function discoverAgenticHubs(engineBaseUrl, {
4349
4438
  * no projects API / not a nano engine, or discovery error/timeout). The
4350
4439
  * worker continues doing real work with no channel.
4351
4440
  *
4352
- * @param {{ fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
4441
+ * @param {{ camunda?: object, fetchImpl?: Function, wsProbe?: Function, timeoutMs?: number }} [opts]
4353
4442
  * @returns {Promise<{ status: string, config?: object, message?: string, candidates?: Array }>}
4354
4443
  */
4355
- async function resolveAgenticTarget(opts = {}) {
4356
- const base = resolveAgenticConfig();
4444
+ async function resolveAgenticTarget({ camunda, ...opts } = {}) {
4445
+ const base = resolveAgenticConfig(camunda);
4357
4446
  if (!base) return { status: 'off' };
4358
4447
  // Explicit target wins verbatim and skips discovery entirely.
4359
4448
  if (base.explicitUrl) return { status: 'connect', config: base };
@@ -4554,7 +4643,12 @@ async function workAgent(req, flags) {
4554
4643
  const n = Number.parseInt(String(v ?? ''), 10);
4555
4644
  return Number.isFinite(n) && n > 0 ? n : dflt;
4556
4645
  };
4557
- const maxParallelJobs = intFlag(flags?.['max-parallel'], 1);
4646
+ // One job per worker, hard-wired (there is deliberately no --max-parallel
4647
+ // flag): an agent harness holds a PTY + a git workspace for the whole life of
4648
+ // a job, so a worker must never lease a second job concurrently. The @camunda8
4649
+ // SDK derives maxJobsToActivate = maxParallelJobs - activeJobs, so 1 means
4650
+ // "activate one job, then stop polling until it completes".
4651
+ const maxParallelJobs = 1;
4558
4652
  // The broker job-activation lock is NOT hardcoded up front. A fixed timeout is
4559
4653
  // impossible to size for an agent: too short reclaims a still-working job (a
4560
4654
  // second agent starts + the stale complete/fail is rejected 409), too long
@@ -4748,12 +4842,13 @@ async function workAgent(req, flags) {
4748
4842
  }
4749
4843
  const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
4750
4844
  logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
4751
- logger.info(` max parallel: ${maxParallelJobs}; recovery window: ${recoveryWindowMs}ms; idle timeout: ${idleTimeoutMs}ms; hard cap: ${hardCapMs > 0 ? `${hardCapMs}ms` : 'off'}; poll timeout: ${pollTimeoutMs}ms`);
4845
+ logger.info(` one job per worker; recovery window: ${recoveryWindowMs}ms; idle timeout: ${idleTimeoutMs}ms; hard cap: ${hardCapMs > 0 ? `${hardCapMs}ms` : 'off'}; poll timeout: ${pollTimeoutMs}ms`);
4752
4846
  // Warm the gh-token cache now, off the job-handling path: githubCloneToken()
4753
4847
  // may consult `gh auth token` (a synchronous spawn, up to 10s) as its default
4754
- // credential fallback, and doing that inside a job handler would stall sibling
4755
- // handlers + lock heartbeats when maxParallelJobs > 1. Priming here pays that
4756
- // cost once at startup so every later lookup is a warm cache hit.
4848
+ // credential fallback, and doing that inside a job handler would block the
4849
+ // event loop stalling the lock heartbeat and delaying the job itself.
4850
+ // Priming here pays that cost once at startup so every later lookup is a warm
4851
+ // cache hit.
4757
4852
  primeGhAuthToken();
4758
4853
  logger.info('Polling for work — press Ctrl-C to stop.');
4759
4854
 
@@ -4774,24 +4869,30 @@ async function workAgent(req, flags) {
4774
4869
  installParentDeathWatchdog({ parentPid: Number.isInteger(daemonPid) ? daemonPid : undefined });
4775
4870
  }
4776
4871
  const activeJobs = new Map(); // jobKey -> { type, since (ms epoch) }
4777
- // Which engine this worker polls jobs from + the live agentic-visibility
4778
- // channel status, both surfaced to `supervisor status` via the activity
4779
- // marker (#99). `agenticState` starts 'starting' and is updated once the
4780
- // channel target is resolved and again on each connect/disconnect below.
4781
- // The engine must name the ACTUAL polling authority: jobs are activated by
4782
- // `camunda.createJobWorker()` against the active c8ctl profile engine
4783
- // (`camunda.getConfig().restAddress`), whereas `restConfig` can honor the
4784
- // auxiliary NANO_REST_URL/NANO_BASE_URL/nanoUrl overrides (for `--auto`
4785
- // reads). Derive from the profile engine, using restConfig only as a
4786
- // fallback, so the column can't advertise an override host jobs aren't
4787
- // polled from.
4788
- const workerEngine = (() => {
4789
- try {
4790
- const profileBase = normalizeRestBase(camunda?.getConfig?.()?.restAddress);
4791
- if (profileBase) return profileBase;
4792
- } catch { /* degrade to the auxiliary REST config below */ }
4793
- return restConfig?.baseUrl || null;
4794
- })();
4872
+ // Which engine this worker polls jobs from, surfaced to `supervisor status` via
4873
+ // the activity marker (#99). Derived from resolveWorkerPollEngineBase the SDK
4874
+ // client's OWN profile restAddress (the base createJobWorker actually activates
4875
+ // against), NOT the NANO_* / cfg.nanoUrl override that resolveWorkerEngineBase
4876
+ // prefers for auxiliary REST reads. That keeps the ENGINE column honest: it
4877
+ // reports the engine the worker truly polls, never an override the job worker
4878
+ // ignores. Falls back to the canonical resolver only when the client exposes no
4879
+ // usable profile base.
4880
+ const workerEngine = resolveWorkerPollEngineBase(camunda);
4881
+ // Base URL for the per-job linked-prompt fetch (issue #63). This follows
4882
+ // resolveLinkedPromptBase — the polling engine (profile restAddress the SDK
4883
+ // activates jobs against), since the linked resourceKey is BROKER-LOCAL and must
4884
+ // be fetched from the broker that issued it — with NANO_REST_URL as the only
4885
+ // explicit escape hatch. It deliberately does NOT follow the NANO_BASE_URL /
4886
+ // cfg.nanoUrl auxiliary-read overrides resolveWorkerEngineBase prefers: pointing
4887
+ // the prompt fetch at a different engine than the activation broker would 404 a
4888
+ // broker-local resourceKey while job activation still succeeds. Computed ONCE at
4889
+ // startup so the per-job hot path skips a config.json read. It coincides with
4890
+ // `workerEngine` (the polling engine) unless NANO_REST_URL is set.
4891
+ const linkedPromptBase = resolveLinkedPromptBase(camunda);
4892
+ // The live agentic-visibility channel status, also surfaced to `supervisor
4893
+ // status` via the activity marker (#99). `agenticState` starts 'starting' and
4894
+ // is updated once the channel target is resolved and again on each
4895
+ // connect/disconnect below.
4795
4896
  let agenticState = { status: 'starting' };
4796
4897
  const writeActivity = () => {
4797
4898
  if (!activityFile) return;
@@ -4843,7 +4944,7 @@ async function workAgent(req, flags) {
4843
4944
  // worker joins with the well-known LOCAL token and no credential; SECURE mode
4844
4945
  // (NANO_AGENTIC_SECRET) sends a real per-peer shared secret as the identity;
4845
4946
  // NANO_AGENTIC=off disables it (see resolveAgenticConfig).
4846
- const agenticTarget = await resolveAgenticTarget({ logger });
4947
+ const agenticTarget = await resolveAgenticTarget({ camunda, logger });
4847
4948
  let agenticCfg = null;
4848
4949
  // buildAgenticUrl can throw on a malformed/unsupported explicit NANO_AGENTIC_URL.
4849
4950
  // This is only the display URL for the activity marker, so compute it
@@ -5012,8 +5113,10 @@ async function workAgent(req, flags) {
5012
5113
  try {
5013
5114
  // Fetch the prompt from the broker the SDK client is connected to,
5014
5115
  // deriving base URL + auth from that client (not restConfig, whose base
5015
- // defaults to localhost) — the resourceKey is broker-local.
5016
- const promptSource = await resolveLinkedPromptSource(camunda);
5116
+ // defaults to localhost) — the resourceKey is broker-local. The base is
5117
+ // invariant, so reuse the once-at-startup linkedPromptBase and let the
5118
+ // resolver only compute per-job auth headers (no per-job config.json read).
5119
+ const promptSource = await resolveLinkedPromptSource(camunda, process.env, { baseUrl: linkedPromptBase });
5017
5120
  const linked = await resolveLinkedPrompt(job.customHeaders ?? {}, {
5018
5121
  baseUrl: promptSource.baseUrl || restConfig.baseUrl,
5019
5122
  authHeaders: promptSource.authHeaders,
@@ -5523,10 +5626,10 @@ const SUPERVISOR_MONITOR_INTERVAL_MS = 1_000;
5523
5626
  // extra daemon traffic.
5524
5627
  const SUPERVISOR_LIVE_TICK_MS = 5_000;
5525
5628
 
5526
- // The `nano work` flags forwarded verbatim to each spawned child.
5629
+ // The `nano work` flags forwarded to each spawned child (reconstructed and
5630
+ // normalized by `reconstructWorkArgs`, not passed through byte-for-byte).
5527
5631
  // kind: 'value' → `--flag v`; 'boolean' → `--flag`; 'list' → repeated `--flag v`.
5528
5632
  const WORK_FORWARD_FLAGS = {
5529
- 'max-parallel': 'value',
5530
5633
  'job-timeout': 'value',
5531
5634
  'recovery-window': 'value',
5532
5635
  'idle-timeout': 'value',
@@ -8852,6 +8955,9 @@ export {
8852
8955
  pickLinkedResource,
8853
8956
  resolveBrokerRestConfig,
8854
8957
  resolveAutoRestConfig,
8958
+ resolveWorkerEngineBase,
8959
+ resolveWorkerPollEngineBase,
8960
+ resolveLinkedPromptBase,
8855
8961
  resourceContentUrl,
8856
8962
  fetchLinkedResourceContent,
8857
8963
  resolveLinkedPrompt,
@@ -9006,7 +9112,7 @@ export const metadata = {
9006
9112
  { command: 'c8ctl nano supervisor start --worker reviewer --worker coder', description: 'Start a detached supervisor managing several workers from one terminal' },
9007
9113
  { command: 'c8ctl nano supervisor', description: 'Attach an interactive console to the supervisor (detach with Ctrl-D, leaving it running)' },
9008
9114
  { command: 'c8ctl nano supervisor status', description: 'List supervised workers (state, ENGINE + AGENTIC visibility diagnostics, serviced job / idle, pid, restarts, uptime) without the console' },
9009
- { command: 'c8ctl nano supervisor add decider --max-parallel 2', description: 'Add a supervised worker (forwarding work flags) to the running supervisor' },
9115
+ { command: 'c8ctl nano supervisor add decider', description: 'Add a supervised worker (forwarding work flags) to the running supervisor' },
9010
9116
  { command: 'c8ctl nano supervisor add reviewer --instances 3', description: 'Add 3 distinct auto-named instances of a profile in one call' },
9011
9117
  { command: 'c8ctl nano supervisor restart reviewer', description: 'Restart a supervised worker by id or profile' },
9012
9118
  { command: 'c8ctl nano supervisor stop', description: 'Stop the supervisor daemon and all its workers' },
@@ -9068,7 +9174,6 @@ export const commands = {
9068
9174
  'keep-runs': { type: 'boolean', description: 'work: keep per-job workspaces under <state>/agent-runs instead of deleting them after each job (debug)' },
9069
9175
  stream: { type: 'boolean', description: 'work: tee each agent job\'s live stdout/stderr to this console, prefixed with the job type + key (spy/debug)' },
9070
9176
  list: { type: 'boolean', description: 'hire: list existing agent profiles instead of creating one' },
9071
- 'max-parallel': { type: 'string', description: 'work: max concurrent jobs per worker (default 1)' },
9072
9177
  'job-timeout': { type: 'string', description: 'work: OPTIONAL absolute hard cap on total harness runtime per job in ms; the process is killed past this. Default 0 = unlimited (the broker lock is auto-managed — see --recovery-window / --idle-timeout).' },
9073
9178
  'recovery-window': { type: 'string', description: 'work: broker activation-lock window in ms, auto-refreshed while the agent runs; also the node-loss reclaim time (a dead/killed worker\'s job is re-activated within this). Default 300000.' },
9074
9179
  'idle-timeout': { type: 'string', description: 'work: max ms an agent may produce no stdout/stderr before it is killed as wedged (stops lock extension → job reclaimed). Default 300000.' },
@@ -9234,7 +9339,7 @@ function printUsage() {
9234
9339
  console.log(' c8ctl nano update [--check]');
9235
9340
  console.log(' c8ctl nano hire [--name <n>] [--rank <r>] [--command <c>] [--arg <switch> ...] [--model <m>] [--capabilities <a,b>] [--sandbox none|docker|podman] [--image <ref>] [--terminal pty|pipe] [--env NAME=VALUE ...] [--list]');
9236
9341
  console.log(' c8ctl nano assign <profileName> <cap[,cap...]> [--name <n>] [--capabilities <a,b>]');
9237
- console.log(' c8ctl nano work <profileName> [--auto [--auto-scope <p>]] [--arg <switch> ...] [--max-parallel <n>] [--recovery-window <ms>] [--idle-timeout <ms>] [--job-timeout <ms>] [--poll-timeout <ms>] [--job-type <token> ...] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--secret-resolver host] [--min-free-mb <n>] [--clone-timeout <ms>] [--keep-runs] [--stream]');
9342
+ console.log(' c8ctl nano work <profileName> [--auto [--auto-scope <p>]] [--arg <switch> ...] [--recovery-window <ms>] [--idle-timeout <ms>] [--job-timeout <ms>] [--poll-timeout <ms>] [--job-type <token> ...] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--secret-resolver host] [--min-free-mb <n>] [--clone-timeout <ms>] [--keep-runs] [--stream]');
9238
9343
  console.log(' c8ctl nano supervisor [start|status|add|remove|restart|stop|logs|attach] ... (manage many workers from one terminal)');
9239
9344
  console.log('');
9240
9345
  console.log('Subcommands:');
@@ -9279,7 +9384,6 @@ function printUsage() {
9279
9384
  console.log(' --terminal <m> hire: live-terminal mode pty|pipe (default pipe); pty streams a steerable terminal on the relay lane');
9280
9385
  console.log(' --env NAME=VALUE hire/work: static env var for the harness (repeatable); persisted on hire, work extends/overrides');
9281
9386
  console.log(' --list hire: list existing agent profiles instead of creating one');
9282
- console.log(' --max-parallel <n> work: max concurrent jobs per worker (default 1)');
9283
9387
  console.log(' --job-type <token> work: extra job type to service alongside the rank×capability matrix (repeatable)');
9284
9388
  console.log(' --auto work: zero-config enrolment — serve ALL deployed agent job types read from the engine (no capability, no app enrol endpoint, no channel). NO capability gate: serves any deployed agent job on the engine.');
9285
9389
  console.log(' --auto-scope <p> work: with --auto, narrow to agent job types whose bpmn:process id equals or is prefixed by <p> (one app/network); default all');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.39.0",
3
+ "version": "1.39.2",
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.39.0",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.39.0",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.39.0",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.39.0",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.39.0",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.39.0",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.39.0"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.39.2",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.39.2",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.39.2",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.39.2",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.39.2",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.39.2",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.39.2"
67
67
  }
68
68
  }