c8ctl-plugin-nano 1.48.0 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ // The concrete host-connection agentic endpoint (ADR 0056; issue #160).
2
+ //
3
+ // #158 shipped the host-owned job-ownership protocol as an Effect *interface*
4
+ // (`AgenticEndpoint` → `AgenticHandle` implementing `register`/`heartbeat`/
5
+ // `deregister`/`claim`/`transcript`/`release`, identity explicit on every frame)
6
+ // but no concrete transport — the `0.10.0` `@nanobpm/agentic` had no `claim`/
7
+ // `release` presence frames to send. `@nanobpm/agentic` is now `^0.11.0`, which
8
+ // lands families 8/9 (`claim`/`release`), the `claim-release`/`multi-instance`
9
+ // negotiable features, and the additive negotiation module. This module is the
10
+ // concrete wire against that bump: ONE multiplexed WebSocket connection over
11
+ // which N supervised workers' presence + ownership + transcript frames ride,
12
+ // each frame carrying its `instance` EXPLICITLY (never the connection id — the
13
+ // assumption #154 structurally breaks).
14
+ //
15
+ // It produces a plain, Effect-free `RawEmitClient` (see `supervisor/src/emit.ts`)
16
+ // that the supervisor bundle lifts into the Effect `AgenticHandle`. Everything on
17
+ // the wire is CONSUMED through this plugin's single import surface (`agentic.mjs`
18
+ // → `@nanobpm/agentic` + `@nanobpm/urban-agent-client`); nothing is re-declared.
19
+ //
20
+ // The supervisor's `superviseAgentic` owns reconnect (it calls the endpoint's
21
+ // `connect` again on every drop) and the claim registry owns replay (the resync
22
+ // re-`register`s + re-`claim`s before transcript resumes), so this client is thin
23
+ // on purpose: one socket, encode-and-send, no buffering or reconnect of its own.
24
+
25
+ import {
26
+ encodeFrame,
27
+ decodeFrame,
28
+ MAX_SEQ,
29
+ validatePayload,
30
+ negotiate,
31
+ LOCAL_ADVERTISEMENT,
32
+ loadAgenticClient,
33
+ } from './agentic.mjs';
34
+ import { buildAgenticUrl } from './work-channel.mjs';
35
+
36
+ // Presence + ownership frames are facts — they ride the CONTROL lane so a
37
+ // transcript storm on the bulk lane can never delay a claim/release (the QoS
38
+ // ordering contract). Transcript chunks ride BULK.
39
+ const CONTROL_LANE = 'control';
40
+ const BULK_LANE = 'bulk';
41
+
42
+ // A transcript stream name that encodes BOTH the owning instance and the jobKey,
43
+ // so two workers' (or two jobs') transcript streams over the one connection can
44
+ // never collide. `encodeURIComponent` on each segment makes the join
45
+ // unambiguous regardless of what characters an instance/jobKey contains.
46
+ function composeTranscriptStream(instance, jobKey) {
47
+ return `t/${encodeURIComponent(instance)}/${encodeURIComponent(jobKey)}`;
48
+ }
49
+
50
+ // Reverse of {@link composeTranscriptStream}. Returns null for a stream that is
51
+ // not one of ours (a foreign/blackboard stream), so an inbound steer frame for
52
+ // an unrecognised stream is dropped rather than misrouted.
53
+ function parseTranscriptStream(stream) {
54
+ if (typeof stream !== 'string') return null;
55
+ const parts = stream.split('/');
56
+ if (parts.length !== 3 || parts[0] !== 't') return null;
57
+ try {
58
+ return { instance: decodeURIComponent(parts[1]), jobKey: decodeURIComponent(parts[2]) };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ // Drop undefined/empty attributes from a declared capability so the enrolment
65
+ // attribute on `register` stays minimal (the S0 validator only requires
66
+ // `capability` to be an object and tolerates extra fields).
67
+ function cleanCapability(capability) {
68
+ const out = {};
69
+ if (capability && typeof capability === 'object') {
70
+ for (const [k, v] of Object.entries(capability)) {
71
+ if (v !== undefined && v !== null && v !== '') out[k] = v;
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ const textDecoder = new TextDecoder();
78
+ const textEncoder = new TextEncoder();
79
+
80
+ // Compute the negotiated protocol against the peer's advertisement. There is no
81
+ // wired advertisement-exchange frame in the `0.11.0` protocol yet, so a caller
82
+ // that has not learned the peer's support passes nothing and we assume the peer
83
+ // matches this build (full support) — additive negotiation still degrades
84
+ // correctly the moment a real remote advertisement is supplied (an old hub that
85
+ // never learned `claim`/`release` yields a negotiation without them, so those
86
+ // methods report unsupported and the adapter omits them).
87
+ function negotiatedSupport(remoteAdvertisement) {
88
+ const negotiated = negotiate(LOCAL_ADVERTISEMENT, remoteAdvertisement ?? LOCAL_ADVERTISEMENT);
89
+ return {
90
+ claimRelease:
91
+ negotiated.supportsFeature('claim-release') &&
92
+ negotiated.supportsFamily('claim') &&
93
+ negotiated.supportsFamily('release'),
94
+ // Inbound steer rides the relay lane as a delivery frame keyed by our
95
+ // transcript-stream naming; it is installable whenever relay is negotiated.
96
+ steer: negotiated.supportsFamily('relay'),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Open one multiplexed host connection and return a {@link RawEmitClient}.
102
+ *
103
+ * @param {object} params
104
+ * @param {string} params.url the resolved `ws(s)://…/agentic?token=…` channel URL
105
+ * @param {import('@nanobpm/urban-agent-client').TransportFactory} params.transportFactory
106
+ * @param {number} params.incarnation producer generation stamped on transcript
107
+ * frames (strictly higher on each successive connection so a reconnected
108
+ * producer fences its stale predecessor on the hub's incarnation ring)
109
+ * @param {{ claimRelease: boolean, steer: boolean }} params.support negotiated capabilities
110
+ * @param {{ warn?: Function, debug?: Function }} [params.logger]
111
+ * @returns {import('./supervisor.dist.js').RawEmitClient}
112
+ */
113
+ function openHostConnection({ url, transportFactory, incarnation, support, logger }) {
114
+ const log = logger || {};
115
+ const openCbs = [];
116
+ const closeCbs = [];
117
+ let steerRoute = null;
118
+ let open = false;
119
+ let hasClosed = false;
120
+ let seq = 0;
121
+
122
+ // Monotonic uint32 sequence with wraparound — the relay resume-from-offset
123
+ // counter; mirrors the client lib's own seq handling.
124
+ const nextSeq = () => {
125
+ const s = seq;
126
+ seq = seq >= MAX_SEQ ? 0 : seq + 1;
127
+ return s;
128
+ };
129
+
130
+ let transport = null;
131
+ let closedFired = false;
132
+
133
+ // Notify registered onClose subscribers at most once, regardless of whether a
134
+ // caller-initiated close() or the transport's own onClose fires first. Higher
135
+ // layers (AgenticHandle.closed) must observe the drop even if the underlying
136
+ // transport delays or omits its close callback.
137
+ const fireClosed = () => {
138
+ if (closedFired) return;
139
+ closedFired = true;
140
+ open = false;
141
+ hasClosed = true;
142
+ for (const cb of closeCbs) cb();
143
+ };
144
+
145
+ const send = (lane, family, payload) => {
146
+ if (!open || transport === null) {
147
+ // Contract: a not-open transport throws synchronously so the adapter can
148
+ // surface a SupervisorError the best-effort caller swallows (the next
149
+ // resync replays it). Guard here too so we never encode into a dead socket.
150
+ throw new Error(`agentic transport not open (cannot send ${family})`);
151
+ }
152
+ const check = validatePayload(family, payload);
153
+ if (!check.ok) {
154
+ const detail = check.errors.map((e) => `${e.code}:${e.message}`).join(', ');
155
+ throw new Error(`invalid ${family} payload — ${detail}`);
156
+ }
157
+ transport.send(encodeFrame({ lane, family, seq: nextSeq(), payload }));
158
+ };
159
+
160
+ const handleInbound = (bytes) => {
161
+ if (steerRoute === null) return;
162
+ let frame;
163
+ try {
164
+ frame = decodeFrame(bytes);
165
+ } catch (err) {
166
+ log.debug?.(`agentic: undecodable inbound frame dropped — ${err?.message || err}`);
167
+ return;
168
+ }
169
+ // Inbound steer rides the relay family as a DELIVERY chunk ({ stream, offset,
170
+ // chunk }, no `op`) whose stream is one of our transcript streams; the stream
171
+ // carries the target instance + jobKey. A frame for a foreign stream is
172
+ // dropped, never misrouted.
173
+ if (frame.family !== 'relay') return;
174
+ const payload = frame.payload;
175
+ if (!payload || typeof payload !== 'object' || 'op' in payload) return;
176
+ const target = parseTranscriptStream(payload.stream);
177
+ if (target === null) return;
178
+ const chunk = typeof payload.chunk === 'string' ? textEncoder.encode(payload.chunk) : new Uint8Array(0);
179
+ try {
180
+ steerRoute(target.instance, target.jobKey, chunk);
181
+ } catch (err) {
182
+ log.debug?.(`agentic: steer route threw — ${err?.message || err}`);
183
+ }
184
+ };
185
+
186
+ transport = transportFactory(url, {
187
+ onOpen() {
188
+ open = true;
189
+ for (const cb of openCbs) cb();
190
+ },
191
+ onFrame(bytes) {
192
+ handleInbound(bytes);
193
+ },
194
+ onClose() {
195
+ fireClosed();
196
+ },
197
+ onError(err) {
198
+ // Non-fatal on its own; a close follows and drives the reconnect. Surface
199
+ // it for diagnosis only.
200
+ log.debug?.(`agentic transport error — ${err?.message || err}`);
201
+ },
202
+ });
203
+
204
+ return {
205
+ register(instance, capability) {
206
+ send(CONTROL_LANE, 'register', { instance, capability: cleanCapability(capability) });
207
+ },
208
+ heartbeat(instance) {
209
+ send(CONTROL_LANE, 'heartbeat', { instance });
210
+ },
211
+ deregister(instance, reason) {
212
+ send(CONTROL_LANE, 'deregister', reason ? { instance, reason } : { instance });
213
+ },
214
+ claim(instance, jobKey) {
215
+ send(CONTROL_LANE, 'claim', { instance, jobKey });
216
+ },
217
+ release(instance, jobKey) {
218
+ send(CONTROL_LANE, 'release', { instance, jobKey });
219
+ },
220
+ transcript(instance, jobKey, chunk) {
221
+ send(BULK_LANE, 'relay', {
222
+ op: 'produce',
223
+ stream: composeTranscriptStream(instance, jobKey),
224
+ incarnation,
225
+ chunk: textDecoder.decode(chunk),
226
+ });
227
+ },
228
+ onSteer(route) {
229
+ steerRoute = route;
230
+ },
231
+ onOpen(cb) {
232
+ // If the transport is already open (injected/synchronous factories can
233
+ // open before this registers), fire immediately — otherwise the queued
234
+ // callback never runs and connect() hangs waiting for `opened`.
235
+ if (open) {
236
+ cb();
237
+ return;
238
+ }
239
+ openCbs.push(cb);
240
+ },
241
+ onClose(cb) {
242
+ // Mirror onOpen: if the transport already closed (injected/synchronous
243
+ // factories can close before this registers), fire immediately —
244
+ // otherwise a close-before-open is never observable and connect() hangs
245
+ // waiting for `opened` (neither onOpen nor onClose would ever run).
246
+ if (hasClosed) {
247
+ cb();
248
+ return;
249
+ }
250
+ closeCbs.push(cb);
251
+ },
252
+ close() {
253
+ // Mark closed and drop the transport reference locally so post-close
254
+ // send() reliably throws (the not-open contract) even if the underlying
255
+ // transport delays or omits its close callback.
256
+ open = false;
257
+ hasClosed = true;
258
+ const t = transport;
259
+ transport = null;
260
+ try {
261
+ t?.close();
262
+ } catch {
263
+ /* idempotent best-effort teardown — never throw on close */
264
+ }
265
+ // Notify subscribers ourselves — never rely on the transport re-entering
266
+ // onClose after a caller-initiated teardown (it may delay or omit it),
267
+ // which would leave AgenticHandle.closed pending indefinitely. Idempotent:
268
+ // a subsequent transport onClose is a no-op.
269
+ fireClosed();
270
+ },
271
+ supportsClaimRelease: support.claimRelease,
272
+ supportsSteer: support.steer,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Build the `RawEmitConnect` factory the supervisor's `makeAgenticEndpoint`
278
+ * lifts into `deps.agenticEndpoint`. Each returned `connect()` opens ONE fresh
279
+ * multiplexed host connection (a reconnect calls it again), stamped with a
280
+ * strictly-increasing incarnation so a reconnected transcript producer fences
281
+ * its stale predecessor.
282
+ *
283
+ * The WebSocket transport is loaded once through the plugin's single import
284
+ * surface (`loadAgenticClient()` — which installs the source→dist resolve hook so
285
+ * the client is importable under stock Node); a test injects its own
286
+ * `transportFactory` and never touches the real client.
287
+ *
288
+ * @param {object} opts
289
+ * @param {string} opts.url the app's HTTP(S) base URL (channel served same-port at `/agentic`)
290
+ * @param {string} [opts.token] ADR 0028 identity token (carried as `?token=`)
291
+ * @param {string} [opts.credential] capability credential (carried as `?capability=`)
292
+ * @param {import('@nanobpm/agentic/protocol').ProtocolAdvertisement} [opts.remoteAdvertisement]
293
+ * the peer's advertised support; omit to assume full support (see {@link negotiatedSupport})
294
+ * @param {number} [opts.incarnationBase] first transcript incarnation (default `Date.now()`)
295
+ * @param {import('@nanobpm/urban-agent-client').TransportFactory} [opts.transportFactory] injectable transport (tests)
296
+ * @param {{ warn?: Function, debug?: Function }} [opts.logger]
297
+ * @returns {Promise<() => import('./supervisor.dist.js').RawEmitClient>} a synchronous `connect` factory
298
+ */
299
+ export async function createRawEmitConnect(opts) {
300
+ const { url, token, credential, remoteAdvertisement, incarnationBase, transportFactory, logger } = opts || {};
301
+ if (typeof url !== 'string' || url.trim() === '') {
302
+ throw new Error('createRawEmitConnect requires an agentic channel base url');
303
+ }
304
+
305
+ const channelUrl = buildAgenticUrl(url, { token, credential });
306
+ const factory = transportFactory ?? (await loadAgenticClient()).websocketTransport;
307
+ const support = negotiatedSupport(remoteAdvertisement);
308
+
309
+ // Strictly-increasing per-connection incarnation so successive reconnects fence
310
+ // their predecessor on the hub's transcript ring (a monotonic takeover counter,
311
+ // seeded from the clock so a later-started process starts ahead).
312
+ let generation = Number.isInteger(incarnationBase) && incarnationBase >= 0 ? incarnationBase : Date.now();
313
+
314
+ return () =>
315
+ openHostConnection({
316
+ url: channelUrl,
317
+ transportFactory: factory,
318
+ incarnation: generation++,
319
+ support,
320
+ logger,
321
+ });
322
+ }
323
+
324
+ // Exposed for unit tests / reuse.
325
+ export { composeTranscriptStream, parseTranscriptStream, negotiatedSupport, cleanCapability };
package/agentic.mjs CHANGED
@@ -58,6 +58,12 @@ export {
58
58
  validateVocabDocument,
59
59
  // per-family payload contracts
60
60
  validatePayload,
61
+ // additive capability/version negotiation (claim/release degrade gracefully)
62
+ PROTOCOL_VERSION,
63
+ PROTOCOL_FEATURES,
64
+ LOCAL_ADVERTISEMENT,
65
+ parseAdvertisement,
66
+ negotiate,
61
67
  // language-neutral hex helpers (used to hold the codec to the corpus)
62
68
  bytesToHex,
63
69
  hexToBytes,
package/c8ctl-plugin.js CHANGED
@@ -3239,6 +3239,172 @@ function loadSupervisorRuntime() {
3239
3239
  return _supervisorRuntime;
3240
3240
  }
3241
3241
 
3242
+ // Build the concrete single host-connection agentic endpoint (issue #160) the
3243
+ // supervisor consumes as `deps.agenticEndpoint`. Composes the raw-JS wire client
3244
+ // (`agentic-endpoint.mjs`, one multiplexed connection over `@nanobpm/agentic`
3245
+ // `^0.11.0`'s `claim`/`release` families, consumed through this plugin's single
3246
+ // import surface) with the supervisor bundle's `makeAgenticEndpoint` adapter,
3247
+ // which lifts the plain client into the Effect `AgenticHandle`. This is the ONE
3248
+ // place the ownership/presence/transcript wire is instantiated for the
3249
+ // single-owner runtime. It is the eventual replacement for the per-worker
3250
+ // `createWorkChannel` fan-out — where every `nano work` process opened its own
3251
+ // socket for a single identity — but that per-worker flip is deferred, so
3252
+ // `createWorkChannel` is still imported and used elsewhere in this file for now.
3253
+ // The ~100 KB Effect surface loads lazily on the first `createAgenticEndpoint()`
3254
+ // call (which awaits `loadSupervisorRuntime()`) — not at module load and not
3255
+ // deferred to `endpoint.connect()` — so a plugin invocation that never runs an
3256
+ // agentic connection never pays for it.
3257
+ //
3258
+ // @param {object} opts see `createRawEmitConnect` in `agentic-endpoint.mjs`
3259
+ // (`url`, `token`, `credential`, optional `remoteAdvertisement`, `logger`, …)
3260
+ // @returns {Promise<import('./supervisor.dist.js').AgenticEndpoint>}
3261
+ async function createAgenticEndpoint(opts) {
3262
+ const { makeAgenticEndpoint } = await loadSupervisorRuntime();
3263
+ const { createRawEmitConnect } = await import('./agentic-endpoint.mjs');
3264
+ const connect = await createRawEmitConnect(opts);
3265
+ return makeAgenticEndpoint(connect);
3266
+ }
3267
+
3268
+ // Compose the monolith's real edges into the single-owner supervisor runtime's
3269
+ // injected ports (issue #156) — the live-cut-over seam that turns the #154
3270
+ // runtime interface into a runnable host owner. This is the ONE place the
3271
+ // concrete `EngineClient`/`ReconcileReader`/`JobRunner`/`Logger` are assembled
3272
+ // for the runtime, the direct analogue of `createAgenticEndpoint` for the
3273
+ // ownership wire:
3274
+ //
3275
+ // - engine → `supervisor-engine.mjs` (direct `POST /v2/jobs/activation`
3276
+ // + `PATCH /v2/jobs/{key}/timeout`), REPLACING the SDK job
3277
+ // worker per the issue, base/auth derived from the SAME
3278
+ // `resolveWorkerEngineBase` chain the reconcile read uses;
3279
+ // - reconcileReader → the existing `defaultC8RestReader` (a keep-alive,
3280
+ // IPv4-first `httpC8RestReader`), whose shape already IS a
3281
+ // `RawReconcileReader`;
3282
+ // - scan → `demand.scanTaskDefinitions` via `scanAgentTaskLeaves`,
3283
+ // the single agentic-leaf source of truth (no drift);
3284
+ // - logger → `getLogger()`;
3285
+ // - runner → the caller's raw `{ run(job): Promise<void> }` (the
3286
+ // `runAgentJob`-owning-completion harness). REQUIRED to
3287
+ // actually execute; a test injects a fake.
3288
+ //
3289
+ // Returns everything a JS caller needs to run `makeSupervisor` — the assembled
3290
+ // `deps`, the seeded `registry` (so workers can be add/remove'd live), and the
3291
+ // `makeSupervisor` + `Effect` handles from the bundle. NOTE (deferred, issue
3292
+ // #156): the actual hot-path flip — deleting the per-type SDK pollers, the
3293
+ // process-wide `singleFlight`, and the per-process reconcile crawl in `workAgent`
3294
+ // and running `makeSupervisor(deps).run` as the single per-host owner — is NOT
3295
+ // wired here; it deletes battle-tested crash-safety code and can only be
3296
+ // validated against a live engine, so it is intentionally left as the follow-up
3297
+ // this seam unblocks. Constructing deps is side-effect-free (no socket opens, no
3298
+ // activation) until the caller forks `supervisor.run`.
3299
+ //
3300
+ // @param {object} opts
3301
+ // @param {{ run(job): Promise<void> }} opts.runner raw job runner (required to run)
3302
+ // @param {object} [opts.camunda] SDK client, for base/auth derivation
3303
+ // @param {{ baseUrl: string, token?: string }} [opts.restConfig] explicit REST config (else derived)
3304
+ // @param {Record<string,string>} [opts.authHeaders] ready-made auth header map
3305
+ // @param {string} [opts.worker] worker id stamped on activations
3306
+ // @param {Array<{id: string, types?: Iterable<string>, capacity?: number}>} [opts.workers] registry seed
3307
+ // @param {string} [opts.autoWorkerId] worker whose types the reconcile loop rewrites
3308
+ // @param {import('./supervisor.dist.js').AgenticEndpoint} [opts.agenticEndpoint] the ownership wire
3309
+ // @param {object} [opts.agenticConfig] reconnect backoff config
3310
+ // @param {string} [opts.scope] reconcile process-id scope narrowing
3311
+ // @param {object} [opts.config] partial SupervisorConfig overrides
3312
+ // @param {{ searchProcessDefinitionKeys: Function, getProcessDefinitionXml: Function }} [opts.reconcileReader] raw reconcile reader overriding the default keep-alive `defaultC8RestReader`
3313
+ // @param {typeof fetch} [opts.fetchImpl] injected fetch (tests)
3314
+ // @param {NodeJS.ProcessEnv} [opts.env]
3315
+ // @returns {Promise<{ deps: object, registry: object, makeSupervisor: Function, Effect: object, Fiber: object }>}
3316
+ async function createSupervisorDeps(opts = {}) {
3317
+ const {
3318
+ runner,
3319
+ camunda,
3320
+ restConfig,
3321
+ authHeaders,
3322
+ worker,
3323
+ workers = [],
3324
+ autoWorkerId,
3325
+ agenticEndpoint,
3326
+ agenticConfig,
3327
+ scope = '',
3328
+ config,
3329
+ fetchImpl,
3330
+ env = process.env,
3331
+ } = opts;
3332
+ if (!runner || typeof runner.run !== 'function') {
3333
+ throw new TypeError('createSupervisorDeps: `runner` must be a raw job runner `{ run(job): Promise<void> }`');
3334
+ }
3335
+
3336
+ const rt = await loadSupervisorRuntime();
3337
+ const { demand } = await import('./agentic.mjs');
3338
+ const { createRawEngineClient } = await import('./supervisor-engine.mjs');
3339
+
3340
+ // Base/auth: the single canonical worker-engine chain (explicit restConfig →
3341
+ // profile restAddress → localhost), ALWAYS run through the token same-origin
3342
+ // gate — even when a caller pins the base via `restConfig` — so token
3343
+ // derivation (env/config REST token, same-origin agentic fallback) is never
3344
+ // bypassed. An explicit `restConfig.token` still wins over the gate's derived
3345
+ // token; a caller passing only `{ baseUrl }` no longer silently skips auth.
3346
+ const gated = resolveBrokerRestConfig(env, {
3347
+ baseUrl:
3348
+ (restConfig && restConfig.baseUrl) ||
3349
+ (camunda ? resolveWorkerEngineBase(camunda, env) : undefined),
3350
+ });
3351
+ const rc = {
3352
+ baseUrl: gated.baseUrl,
3353
+ token: (restConfig && restConfig.token) || gated.token,
3354
+ };
3355
+
3356
+ // Auth: an explicit ready-made `authHeaders` map wins; else a bare REST token
3357
+ // (`rc.token`) is applied by the engine client itself; else — on OAuth/basic
3358
+ // profiles where no bare token exists — derive the headers from the SDK client's
3359
+ // `getAuthHeaders()` (mirroring resolveLinkedPromptSource). Guarded so an older or
3360
+ // atypical client runtime degrades to the token/none path rather than throw.
3361
+ let resolvedAuthHeaders = authHeaders;
3362
+ if (!resolvedAuthHeaders && !rc.token && camunda && typeof camunda.getAuthHeaders === 'function') {
3363
+ // Resolve PER CALL, not once at startup: SDK auth headers (e.g. an OAuth
3364
+ // bearer) rotate on token refresh, so a long-lived supervisor must re-derive
3365
+ // them on every engine call — a cached startup snapshot would silently expire
3366
+ // and fail activations. Pass a resolver the engine client invokes per request.
3367
+ resolvedAuthHeaders = async () => {
3368
+ try {
3369
+ return await camunda.getAuthHeaders();
3370
+ } catch {
3371
+ return undefined;
3372
+ }
3373
+ };
3374
+ }
3375
+
3376
+ const engine = rt.makeEngineClient(
3377
+ createRawEngineClient({ baseUrl: rc.baseUrl, token: rc.token, authHeaders: resolvedAuthHeaders, worker, fetchImpl }),
3378
+ );
3379
+ // `reconcileReader` (a raw `{ searchProcessDefinitionKeys, getProcessDefinitionXml }`)
3380
+ // may be injected to override the default keep-alive `httpC8RestReader` — the
3381
+ // caller may already hold one, and a test drives the crawl without a socket.
3382
+ const rawReader = opts.reconcileReader || (await defaultC8RestReader(rc));
3383
+ const reconcileReader = rt.makeReconcileReader(rawReader);
3384
+ const scan = (xml) => scanAgentTaskLeaves(xml, demand.scanTaskDefinitions);
3385
+ const logger = rt.asLogger(getLogger());
3386
+
3387
+ const registry = await rt.Effect.runPromise(rt.makeRegistry());
3388
+ for (const w of workers) {
3389
+ await rt.Effect.runPromise(registry.add(w.id, w.types || [], w.capacity ?? 1));
3390
+ }
3391
+
3392
+ const deps = rt.makeSupervisorDeps({
3393
+ engine,
3394
+ runner: rt.makeJobRunner(runner),
3395
+ registry,
3396
+ reconcileReader,
3397
+ scan,
3398
+ logger,
3399
+ autoWorkerId,
3400
+ agenticEndpoint,
3401
+ agenticConfig,
3402
+ config: scope ? { ...config, scope } : config,
3403
+ });
3404
+
3405
+ return { deps, registry, makeSupervisor: rt.makeSupervisor, Effect: rt.Effect, Fiber: rt.Fiber };
3406
+ }
3407
+
3242
3408
  // Build the content endpoint. Per issue #63 / nano-bpm #759 the non-binary
3243
3409
  // `/content` variant is deprecated for non-RPA types (Markdown → 406), so the
3244
3410
  // worker always fetches `/content/binary`.
@@ -12556,6 +12722,8 @@ export {
12556
12722
  readDeployedAgentJobTypes,
12557
12723
  resolveAutoJobTypes,
12558
12724
  loadSupervisorRuntime,
12725
+ createAgenticEndpoint,
12726
+ createSupervisorDeps,
12559
12727
  enableEngineHappyEyeballs,
12560
12728
  preferIpv4Resolution,
12561
12729
  ipv4FirstNodeOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.48.0",
3
+ "version": "1.50.0",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -24,6 +24,8 @@
24
24
  "platforms.mjs",
25
25
  "agentic.mjs",
26
26
  "agentic-loader-hook.mjs",
27
+ "agentic-endpoint.mjs",
28
+ "supervisor-engine.mjs",
27
29
  "work-channel.mjs",
28
30
  "work-relay.mjs",
29
31
  "work-buffer.mjs",
@@ -67,12 +69,12 @@
67
69
  },
68
70
  "optionalDependencies": {
69
71
  "node-pty": "^1.0.0",
70
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.48.0",
71
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.48.0",
72
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.48.0",
73
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.48.0",
74
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.48.0",
75
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.48.0",
76
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.48.0"
72
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.50.0",
73
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.50.0",
74
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.50.0",
75
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.50.0",
76
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.50.0",
77
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.50.0",
78
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.50.0"
77
79
  }
78
80
  }