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 +8 -7
- package/c8ctl-plugin.js +176 -72
- package/package.json +8 -8
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 --
|
|
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
|
|
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`**
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
|
2653
|
-
//
|
|
2654
|
-
//
|
|
2655
|
-
//
|
|
2656
|
-
//
|
|
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
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
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,
|
|
2987
|
-
//
|
|
2988
|
-
//
|
|
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;
|
|
4087
|
-
* 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.
|
|
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
|
|
4102
|
-
//
|
|
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
|
-
||
|
|
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
|
-
|
|
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(`
|
|
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
|
|
4755
|
-
//
|
|
4756
|
-
// cost once at startup so every later lookup is a warm
|
|
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
|
|
4778
|
-
//
|
|
4779
|
-
//
|
|
4780
|
-
//
|
|
4781
|
-
//
|
|
4782
|
-
//
|
|
4783
|
-
//
|
|
4784
|
-
//
|
|
4785
|
-
|
|
4786
|
-
//
|
|
4787
|
-
//
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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> ...] [--
|
|
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.
|
|
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.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.39.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.39.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.39.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.39.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.39.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.39.
|
|
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
|
}
|