c8ctl-plugin-nano 1.17.0 → 1.18.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/README.md +31 -0
- package/c8ctl-plugin.js +139 -13
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -150,6 +150,9 @@ c8ctl nano hire
|
|
|
150
150
|
c8ctl nano hire --name reviewer --rank senior --command copilot \
|
|
151
151
|
--model gpt-5 --capabilities code-review,testing
|
|
152
152
|
|
|
153
|
+
# Give the harness command-line switches (e.g. run copilot with --allow-all)
|
|
154
|
+
c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all
|
|
155
|
+
|
|
153
156
|
# List profiles
|
|
154
157
|
c8ctl nano hire --list
|
|
155
158
|
```
|
|
@@ -204,6 +207,16 @@ the job with a decremented retry count, and a job that outlives `--job-timeout`
|
|
|
204
207
|
is killed. Profiles are stored in the plugin's `config.json` (see `c8ctl nano
|
|
205
208
|
config`).
|
|
206
209
|
|
|
210
|
+
> **Activation lock vs. kill deadline.** `--job-timeout` is the harness *kill*
|
|
211
|
+
> deadline. The broker's job-activation lock is derived as `--job-timeout +
|
|
212
|
+
> --lock-grace` (grace defaults to `120000`ms) so it strictly outlasts the kill:
|
|
213
|
+
> the worker always reports the outcome (complete/fail) before the lock lapses.
|
|
214
|
+
> Without that gap a lock expiring exactly as the harness dies lets the broker
|
|
215
|
+
> re-activate the still-retryable job (a second agent starts) and the stale
|
|
216
|
+
> `fail` is rejected with a 409 "job cannot be failed in the current state".
|
|
217
|
+
> Raising `--job-timeout` alone does **not** fix this — it moves both coupled
|
|
218
|
+
> deadlines together; widen `--lock-grace` (or keep the default) instead.
|
|
219
|
+
|
|
207
220
|
> **Trust boundary.** The profile `command` is run through a shell so you can
|
|
208
221
|
> write a full invocation (args, pipes, multi-word commands). It is
|
|
209
222
|
> **operator-authored** — only what you put in your own `config.json` is
|
|
@@ -327,6 +340,24 @@ envelope layers on top (job-specific tuning wins), and the reserved `AGENT_*`
|
|
|
327
340
|
variables and resolved secrets always win over user-supplied env so they can't be
|
|
328
341
|
shadowed. For **secret** values use `secretRefs`, not `--env`.
|
|
329
342
|
|
|
343
|
+
**Command-line switches.** Some harnesses take switches rather than env vars —
|
|
344
|
+
e.g. `copilot --allow-all`. Append them to the harness command with a repeatable
|
|
345
|
+
`--arg` (each `--arg` is one argv token). They are persisted on the profile at
|
|
346
|
+
hire time and can be extended at work time:
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all
|
|
350
|
+
c8ctl nano work coder --arg --verbose # appends to the profile args
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The command is spawned through a shell (so `command` still resolves on `PATH`),
|
|
354
|
+
but each `--arg` is shell-quoted as a single literal token, so a value with
|
|
355
|
+
spaces or shell metacharacters can't break out or inject. They apply on the
|
|
356
|
+
container path on any OS, and on the host path on POSIX systems. **On a Windows
|
|
357
|
+
host (`sandbox=none`), `--arg` is rejected** with a clear error — the POSIX
|
|
358
|
+
quoting isn't honoured by `cmd.exe` — so use a container sandbox
|
|
359
|
+
(`--sandbox docker|podman`) or bake the switches into `--command` there.
|
|
360
|
+
|
|
330
361
|
**Disk hygiene.** Host job **workspaces** and container sandboxes both get
|
|
331
362
|
automatic cleanup so leaked artifacts can't fill the disk. Workspaces under
|
|
332
363
|
`<state>/agent-runs` are removed after each job and swept at startup + on
|
package/c8ctl-plugin.js
CHANGED
|
@@ -1416,6 +1416,39 @@ function parseEnvPairs(input) {
|
|
|
1416
1416
|
return { env, errors };
|
|
1417
1417
|
}
|
|
1418
1418
|
|
|
1419
|
+
// Normalize a stored/CLI argument list (string | string[]) into a clean string[]:
|
|
1420
|
+
// each entry is one whole argv token (e.g. "--allow-all"), coerced to a string,
|
|
1421
|
+
// with null/undefined and empty tokens dropped. Interior whitespace is preserved
|
|
1422
|
+
// so a single arg may carry a value like "--foo=a b" intact.
|
|
1423
|
+
function normalizeArgList(input) {
|
|
1424
|
+
const list = input == null ? [] : (Array.isArray(input) ? input : [input]);
|
|
1425
|
+
const out = [];
|
|
1426
|
+
for (const item of list) {
|
|
1427
|
+
if (item == null) continue;
|
|
1428
|
+
const s = String(item);
|
|
1429
|
+
if (s.length === 0) continue;
|
|
1430
|
+
out.push(s);
|
|
1431
|
+
}
|
|
1432
|
+
return out;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// POSIX single-quote a string so it survives `sh -c`/shell:true as one literal
|
|
1436
|
+
// argv token, no matter what it contains (spaces, $, quotes, globs). Empty
|
|
1437
|
+
// string → ''. This is what keeps structured `--arg` values injection-safe even
|
|
1438
|
+
// though the harness is spawned through a shell (for PATH resolution).
|
|
1439
|
+
function shQuote(s) {
|
|
1440
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Build the shell command line for the agent harness: the base command followed
|
|
1444
|
+
// by each structured argument, shell-quoted. With no args the command is used
|
|
1445
|
+
// verbatim (preserving pre-existing hires that baked switches into the command).
|
|
1446
|
+
function buildAgentCommandLine(command, args) {
|
|
1447
|
+
const list = normalizeArgList(args);
|
|
1448
|
+
if (list.length === 0) return command;
|
|
1449
|
+
return `${command} ${list.map(shQuote).join(' ')}`;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1419
1452
|
// A worker job-type token: rank/capability tokens use `:` (rank↔cap) and `+`
|
|
1420
1453
|
// (combined caps) as delimiters, and code-first `@nanobpm/workflow` job types
|
|
1421
1454
|
// are `<flowId>:<taskName>` or an explicit override. The first character must be
|
|
@@ -1445,6 +1478,46 @@ function parseJobTypeFlags(input) {
|
|
|
1445
1478
|
return { jobTypes, errors };
|
|
1446
1479
|
}
|
|
1447
1480
|
|
|
1481
|
+
/**
|
|
1482
|
+
* The broker job-activation lock must strictly outlast the harness kill
|
|
1483
|
+
* deadline: the worker has to report the outcome (complete/fail) before the lock
|
|
1484
|
+
* lapses, or the broker re-activates the still-retryable job (a second agent
|
|
1485
|
+
* starts) and the stale `fail` is rejected 409 "job cannot be failed in the
|
|
1486
|
+
* current state". So lock = kill + grace. Non-finite or non-positive inputs
|
|
1487
|
+
* fall back to fixed defaults (5m kill / 2m grace). All inputs are coerced to
|
|
1488
|
+
* safe positive integers (positive fractional values floor to at least 1) and
|
|
1489
|
+
* grace is capped so a positive kill always fits and `kill + grace` stays within
|
|
1490
|
+
* the safe-integer range, so the invariant lock > kill holds strictly for every
|
|
1491
|
+
* accepted input — including values at or beyond 2^53 where float addition
|
|
1492
|
+
* would otherwise round `kill + grace` back down to `kill`.
|
|
1493
|
+
*
|
|
1494
|
+
* Returns BOTH derived deadlines from this single computation so the caller
|
|
1495
|
+
* never re-derives (and drifts): `killMs` is the *clamped* harness kill deadline
|
|
1496
|
+
* the caller must actually enforce, and `lockMs` is the broker activation lock.
|
|
1497
|
+
* The caller must use `killMs` — not the raw input — for the harness timeout, or
|
|
1498
|
+
* the lock > kill invariant breaks for large inputs (the raw input can exceed
|
|
1499
|
+
* the clamped `killMs`, and thus reach or exceed `lockMs`).
|
|
1500
|
+
*
|
|
1501
|
+
* @returns {{ killMs: number, lockMs: number }}
|
|
1502
|
+
*/
|
|
1503
|
+
function deriveJobLockMs(jobTimeoutMs, lockGraceMs) {
|
|
1504
|
+
const MAX = Number.MAX_SAFE_INTEGER;
|
|
1505
|
+
const toSafeMs = (value, fallback) => {
|
|
1506
|
+
// Floor positive fractional values to at least 1 so a sub-millisecond input
|
|
1507
|
+
// (e.g. 0.5) never collapses to a non-positive value.
|
|
1508
|
+
const n = Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback;
|
|
1509
|
+
return Math.min(n, MAX);
|
|
1510
|
+
};
|
|
1511
|
+
// Cap grace to MAX - 1 so there is always room for a positive kill while
|
|
1512
|
+
// keeping kill + grace within the safe-integer range.
|
|
1513
|
+
const grace = Math.min(toSafeMs(lockGraceMs, 2 * 60_000), MAX - 1);
|
|
1514
|
+
// Cap kill so kill + grace stays a safe integer and floor it at 1 so the
|
|
1515
|
+
// internal kill is always positive; the sum is then exact and strictly
|
|
1516
|
+
// greater than kill (never equal to it via float rounding).
|
|
1517
|
+
const killMs = Math.max(1, Math.min(toSafeMs(jobTimeoutMs, 5 * 60_000), MAX - grace));
|
|
1518
|
+
return { killMs, lockMs: killMs + grace };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1448
1521
|
/** A profile name must be a safe, filesystem/token-friendly slug. */
|
|
1449
1522
|
function isValidProfileName(name) {
|
|
1450
1523
|
return typeof name === 'string' && /^[a-z0-9][a-z0-9._-]*$/i.test(name);
|
|
@@ -1513,6 +1586,7 @@ function normalizeStoredProfile(name, profile) {
|
|
|
1513
1586
|
name,
|
|
1514
1587
|
rank,
|
|
1515
1588
|
command,
|
|
1589
|
+
args: normalizeArgList(profile.args),
|
|
1516
1590
|
model: typeof profile.model === 'string' ? profile.model.trim() : '',
|
|
1517
1591
|
capabilities: normalizeCapabilities(profile.capabilities),
|
|
1518
1592
|
sandbox,
|
|
@@ -1541,7 +1615,7 @@ async function hireWorker(req, flags) {
|
|
|
1541
1615
|
logger.info('Hired agent profiles:');
|
|
1542
1616
|
for (const name of names.sort()) {
|
|
1543
1617
|
const p = hires[name];
|
|
1544
|
-
logger.info(` ${name} [${p.rank}] ${p.command} (model: ${p.model || '-'}; caps: ${normalizeCapabilities(p.capabilities).join(', ') || '-'})`);
|
|
1618
|
+
logger.info(` ${name} [${p.rank}] ${buildAgentCommandLine(p.command, p.args)} (model: ${p.model || '-'}; caps: ${normalizeCapabilities(p.capabilities).join(', ') || '-'})`);
|
|
1545
1619
|
}
|
|
1546
1620
|
logger.info('');
|
|
1547
1621
|
logger.info('Put one to work with: c8ctl nano work <name>');
|
|
@@ -1557,11 +1631,14 @@ async function hireWorker(req, flags) {
|
|
|
1557
1631
|
let capabilities = flags?.capabilities !== undefined ? flags.capabilities : undefined;
|
|
1558
1632
|
let sandbox = flags?.sandbox !== undefined ? String(flags.sandbox).trim().toLowerCase() : undefined;
|
|
1559
1633
|
let image = flags?.image !== undefined ? String(flags.image).trim() : undefined;
|
|
1634
|
+
// Structured command-line switches appended to the command when spawned, e.g.
|
|
1635
|
+
// `--arg --allow-all` for `copilot`. Repeatable; each --arg is one argv token.
|
|
1636
|
+
const commandArgs = normalizeArgList(flags?.arg);
|
|
1560
1637
|
const envFromFlags = flags?.env !== undefined;
|
|
1561
1638
|
const { env: profileEnv, errors: envErrors } = parseEnvPairs(flags?.env);
|
|
1562
1639
|
if (envErrors.length > 0) {
|
|
1563
1640
|
logger.error(envErrors.join('; '));
|
|
1564
|
-
logger.info('Example: c8ctl nano hire --name coder --rank senior --command copilot --
|
|
1641
|
+
logger.info('Example: c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all --env COPILOT_ENABLE_ALL_TOOLS=1');
|
|
1565
1642
|
process.exit(1);
|
|
1566
1643
|
}
|
|
1567
1644
|
|
|
@@ -1659,6 +1736,7 @@ async function hireWorker(req, flags) {
|
|
|
1659
1736
|
name,
|
|
1660
1737
|
rank,
|
|
1661
1738
|
command,
|
|
1739
|
+
args: commandArgs,
|
|
1662
1740
|
model: model || '',
|
|
1663
1741
|
capabilities: normalizeCapabilities(capabilities),
|
|
1664
1742
|
sandbox,
|
|
@@ -1669,9 +1747,10 @@ async function hireWorker(req, flags) {
|
|
|
1669
1747
|
writeHire(profile);
|
|
1670
1748
|
|
|
1671
1749
|
const matrix = jobTypeMatrix(profile.rank, profile.capabilities);
|
|
1672
|
-
logger.info(`${existed ? 'Updated' : 'Hired'} "${name}" [${profile.rank}] → ${profile.command}`);
|
|
1750
|
+
logger.info(`${existed ? 'Updated' : 'Hired'} "${name}" [${profile.rank}] → ${buildAgentCommandLine(profile.command, profile.args)}`);
|
|
1673
1751
|
logger.info(` model: ${profile.model || '(none)'}`);
|
|
1674
1752
|
logger.info(` capabilities: ${profile.capabilities.join(', ') || '(none)'}`);
|
|
1753
|
+
if (profile.args.length > 0) logger.info(` args: ${profile.args.map(shQuote).join(' ')}`);
|
|
1675
1754
|
logger.info(` sandbox: ${profile.sandbox}${CONTAINER_SANDBOXES.has(profile.sandbox) ? ` (image ${profile.image})` : ''}`);
|
|
1676
1755
|
const envKeys = Object.keys(profile.env);
|
|
1677
1756
|
if (envKeys.length > 0) logger.info(` env: ${envKeys.join(', ')}`);
|
|
@@ -2450,9 +2529,13 @@ function baseAgentEnv(profile, job) {
|
|
|
2450
2529
|
* Both paths resolve to the same result contract.
|
|
2451
2530
|
*/
|
|
2452
2531
|
function runAgentJob(profile, job, opts = {}) {
|
|
2453
|
-
const { timeoutMs, envelope, sandbox = 'none', image, runId, secretEnv = {}, passThroughSecretNames = [], cwd, extraEnv = {}, profileEnv = {}, resultFile, stream = false, streamPrefix = '', onStreamOut, onStreamErr } = opts;
|
|
2532
|
+
const { timeoutMs, envelope, sandbox = 'none', image, runId, secretEnv = {}, passThroughSecretNames = [], cwd, extraEnv = {}, profileEnv = {}, resultFile, stream = false, streamPrefix = '', onStreamOut, onStreamErr, args: commandArgs } = opts;
|
|
2454
2533
|
const payload = JSON.stringify(buildAgentPayload(profile, job, envelope));
|
|
2455
2534
|
const agentEnv = baseAgentEnv(profile, job);
|
|
2535
|
+
// The harness command line: the profile command plus its structured switches
|
|
2536
|
+
// (persisted `--arg`s, possibly extended at work time via opts.args), each
|
|
2537
|
+
// shell-quoted. Spawned through a shell so `command` still resolves on PATH.
|
|
2538
|
+
const commandLine = buildAgentCommandLine(profile.command, commandArgs ?? profile.args);
|
|
2456
2539
|
// Static, non-secret env for the harness: the worker/profile's env (e.g. a
|
|
2457
2540
|
// harness's permission toggles) plus the per-job envelope's setup.env
|
|
2458
2541
|
// (job-specific tuning wins over the profile default). Reserved AGENT_* and
|
|
@@ -2461,9 +2544,16 @@ function runAgentJob(profile, job, opts = {}) {
|
|
|
2461
2544
|
|
|
2462
2545
|
if (!CONTAINER_SANDBOXES.has(sandbox)) {
|
|
2463
2546
|
// Host: hand the agent the result file by its real path.
|
|
2547
|
+
// Defense in depth: --arg tokens are POSIX single-quoted, which cmd.exe on
|
|
2548
|
+
// a Windows host does not honour, so args would be mis-parsed under the
|
|
2549
|
+
// shell:true spawn. workAgent already rejects this at startup, but guard the
|
|
2550
|
+
// spawn site too so the invariant holds for any direct caller of runAgentJob.
|
|
2551
|
+
if (commandLine !== profile.command && process.platform === 'win32') {
|
|
2552
|
+
return Promise.resolve({ ok: false, exitCode: null, stdout: '', stderr: '', error: 'command-line args (--arg) are not supported for host execution on Windows; use a container sandbox or bake switches into the command', truncated: false, stderrTruncated: false });
|
|
2553
|
+
}
|
|
2464
2554
|
const resultEnv = resultFile ? { [AGENT_RESULT_FILE_ENV]: resultFile } : {};
|
|
2465
2555
|
return spawnCaptureOneShot({
|
|
2466
|
-
command:
|
|
2556
|
+
command: commandLine,
|
|
2467
2557
|
shell: true,
|
|
2468
2558
|
// Own process group so the timeout handler can kill the whole tree.
|
|
2469
2559
|
detached: process.platform !== 'win32',
|
|
@@ -2516,7 +2606,7 @@ function runAgentJob(profile, job, opts = {}) {
|
|
|
2516
2606
|
...mountArgs,
|
|
2517
2607
|
...envArgs,
|
|
2518
2608
|
image,
|
|
2519
|
-
'sh', '-c',
|
|
2609
|
+
'sh', '-c', commandLine,
|
|
2520
2610
|
];
|
|
2521
2611
|
|
|
2522
2612
|
return spawnCaptureOneShot({
|
|
@@ -2618,12 +2708,28 @@ async function workAgent(req, flags) {
|
|
|
2618
2708
|
}
|
|
2619
2709
|
const profileEnv = { ...profile.env, ...workEnv };
|
|
2620
2710
|
|
|
2711
|
+
// Structured command-line switches: the profile's persisted `--arg`s, extended
|
|
2712
|
+
// by any work-time `--arg` (appended). Lets an operator add switches (e.g.
|
|
2713
|
+
// `--allow-all`) at dispatch time without re-hiring.
|
|
2714
|
+
const effectiveArgs = [...profile.args, ...normalizeArgList(flags?.arg)];
|
|
2715
|
+
|
|
2621
2716
|
const intFlag = (v, dflt) => {
|
|
2622
2717
|
const n = Number.parseInt(String(v ?? ''), 10);
|
|
2623
2718
|
return Number.isFinite(n) && n > 0 ? n : dflt;
|
|
2624
2719
|
};
|
|
2625
2720
|
const maxParallelJobs = intFlag(flags?.['max-parallel'], 1);
|
|
2626
2721
|
const jobTimeoutMs = intFlag(flags?.['job-timeout'], 5 * 60_000);
|
|
2722
|
+
// The broker's job-activation lock MUST outlast the harness kill deadline: the
|
|
2723
|
+
// worker has to report the outcome (complete/fail) before the lock lapses,
|
|
2724
|
+
// otherwise the broker re-activates the still-retryable job (a second agent
|
|
2725
|
+
// starts) and the stale `fail` is rejected with a 409 "job cannot be failed in
|
|
2726
|
+
// the current state". So lock = harness kill + grace. Raising --job-timeout
|
|
2727
|
+
// alone does not help — it moves both coupled deadlines together.
|
|
2728
|
+
const lockGraceMs = intFlag(flags?.['lock-grace'], 2 * 60_000);
|
|
2729
|
+
// deriveJobLockMs is the single source of truth for both deadlines: the harness
|
|
2730
|
+
// MUST enforce the clamped `jobKillMs` (not the raw --job-timeout), or a very
|
|
2731
|
+
// large --job-timeout would outlive the broker lock and re-break the invariant.
|
|
2732
|
+
const { killMs: jobKillMs, lockMs: jobLockMs } = deriveJobLockMs(jobTimeoutMs, lockGraceMs);
|
|
2627
2733
|
|
|
2628
2734
|
// Sandbox: flag overrides the stored profile default. `none` runs on the host
|
|
2629
2735
|
// (legacy); `docker`/`podman` run each job in a throwaway labelled container.
|
|
@@ -2638,6 +2744,17 @@ async function workAgent(req, flags) {
|
|
|
2638
2744
|
logger.error(`--sandbox ${sandbox} requires an --image (or hire the profile with --image).`);
|
|
2639
2745
|
process.exit(1);
|
|
2640
2746
|
}
|
|
2747
|
+
// Structured --arg tokens are POSIX single-quoted (shQuote) for the harness
|
|
2748
|
+
// shell. On the host path that shell is the platform default — cmd.exe on
|
|
2749
|
+
// Windows, which does not honour single quotes — so the quoting would leak
|
|
2750
|
+
// literal quote characters and mis-parse the switches. The container path
|
|
2751
|
+
// always targets the image's `sh`, so it stays correct regardless of host OS.
|
|
2752
|
+
// Fail fast with actionable guidance rather than silently corrupting argv.
|
|
2753
|
+
if (!isContainer && effectiveArgs.length > 0 && process.platform === 'win32') {
|
|
2754
|
+
logger.error('--arg is not supported for host execution on Windows (cmd.exe does not honour POSIX quoting).');
|
|
2755
|
+
logger.error('Use a container sandbox (--sandbox docker|podman --image <ref>) or bake the switches into --command.');
|
|
2756
|
+
process.exit(1);
|
|
2757
|
+
}
|
|
2641
2758
|
|
|
2642
2759
|
const secretResolver = makeSecretResolver(flags?.['secret-resolver']);
|
|
2643
2760
|
if (!secretResolver) {
|
|
@@ -2713,14 +2830,14 @@ async function workAgent(req, flags) {
|
|
|
2713
2830
|
const jobTypes = [...new Set([...matrix, ...extraJobTypes])];
|
|
2714
2831
|
const camunda = globalThis.c8ctl.createClient();
|
|
2715
2832
|
|
|
2716
|
-
logger.info(`Putting "${name}" [${profile.rank}] to work → ${profile.command}`);
|
|
2833
|
+
logger.info(`Putting "${name}" [${profile.rank}] to work → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
|
|
2717
2834
|
logger.info(` model: ${profile.model || '(none)'}; capabilities: ${profile.capabilities.join(', ') || '(none)'}`);
|
|
2718
2835
|
logger.info(` sandbox: ${sandbox}${isContainer ? ` (image ${image})` : ''}`);
|
|
2719
2836
|
const profileEnvKeys = Object.keys(profileEnv);
|
|
2720
2837
|
if (profileEnvKeys.length > 0) logger.info(` harness env: ${profileEnvKeys.join(', ')}`);
|
|
2721
2838
|
const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
|
|
2722
2839
|
logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
|
|
2723
|
-
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${
|
|
2840
|
+
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobKillMs}ms; activation lock: ${jobLockMs}ms`);
|
|
2724
2841
|
logger.info('Polling for work — press Ctrl-C to stop.');
|
|
2725
2842
|
|
|
2726
2843
|
const workers = jobTypes.map((jobType) =>
|
|
@@ -2728,9 +2845,9 @@ async function workAgent(req, flags) {
|
|
|
2728
2845
|
jobType,
|
|
2729
2846
|
workerName: `${name}:${jobType}`,
|
|
2730
2847
|
maxParallelJobs,
|
|
2731
|
-
jobTimeoutMs,
|
|
2848
|
+
jobTimeoutMs: jobLockMs,
|
|
2732
2849
|
jobHandler: async (job) => {
|
|
2733
|
-
logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${profile.command}`);
|
|
2850
|
+
logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
|
|
2734
2851
|
|
|
2735
2852
|
// Disk-budget admission shed: if the engine data root is below the free
|
|
2736
2853
|
// floor, don't start a container — fail (retryable) so work sheds until
|
|
@@ -2811,7 +2928,7 @@ async function workAgent(req, flags) {
|
|
|
2811
2928
|
} catch { resultDir = null; resultFile = null; }
|
|
2812
2929
|
|
|
2813
2930
|
result = await runAgentJob(profile, job, {
|
|
2814
|
-
timeoutMs:
|
|
2931
|
+
timeoutMs: jobKillMs,
|
|
2815
2932
|
envelope,
|
|
2816
2933
|
sandbox,
|
|
2817
2934
|
image,
|
|
@@ -2824,6 +2941,7 @@ async function workAgent(req, flags) {
|
|
|
2824
2941
|
resultFile,
|
|
2825
2942
|
stream,
|
|
2826
2943
|
streamPrefix: `[${jobType} ${job.jobKey}] `,
|
|
2944
|
+
args: effectiveArgs,
|
|
2827
2945
|
// Route the --stream tee through c8ctl's output-mode-aware logger so
|
|
2828
2946
|
// spying never corrupts a structured/JSON output mode.
|
|
2829
2947
|
onStreamOut: stream ? (line) => logger.info(line) : undefined,
|
|
@@ -4273,6 +4391,9 @@ export {
|
|
|
4273
4391
|
sanitizeResultVars,
|
|
4274
4392
|
parseEnvPairs,
|
|
4275
4393
|
normalizeEnvMap,
|
|
4394
|
+
normalizeArgList,
|
|
4395
|
+
shQuote,
|
|
4396
|
+
buildAgentCommandLine,
|
|
4276
4397
|
reapAgentContainers,
|
|
4277
4398
|
diskBudgetOk,
|
|
4278
4399
|
containerEngineAvailable,
|
|
@@ -4288,6 +4409,7 @@ export {
|
|
|
4288
4409
|
normalizeStoredProfile,
|
|
4289
4410
|
jobTypeMatrix,
|
|
4290
4411
|
parseJobTypeFlags,
|
|
4412
|
+
deriveJobLockMs,
|
|
4291
4413
|
AGENT_TASK_NS,
|
|
4292
4414
|
AGENT_RESULT_KEY,
|
|
4293
4415
|
RESULT_SENTINEL,
|
|
@@ -4327,6 +4449,7 @@ export const metadata = {
|
|
|
4327
4449
|
{ command: 'c8ctl nano update --check', description: 'Check whether a newer nano release is available' },
|
|
4328
4450
|
{ command: 'c8ctl nano hire', description: 'Interactively create a CLI agent worker profile (name, rank, command, model, capabilities)' },
|
|
4329
4451
|
{ command: 'c8ctl nano hire --name reviewer --rank senior --command copilot --model gpt-5 --capabilities code-review,testing', description: 'Create a profile non-interactively' },
|
|
4452
|
+
{ command: 'c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all', description: 'Hire copilot with a command-line switch (copilot --allow-all)' },
|
|
4330
4453
|
{ command: 'c8ctl nano hire --name coder --rank senior --command copilot --env COPILOT_ENABLE_ALL_TOOLS=1', description: 'Persist a harness startup env var (e.g. permissions) on the profile' },
|
|
4331
4454
|
{ command: 'c8ctl nano hire --list', description: 'List hired agent profiles' },
|
|
4332
4455
|
{ command: 'c8ctl nano hire --name coder --rank senior --command "agent-harness" --sandbox docker --image ghcr.io/acme/agent:1', description: 'Create a profile that runs each job in a throwaway Docker container' },
|
|
@@ -4375,6 +4498,7 @@ export const commands = {
|
|
|
4375
4498
|
name: { type: 'string', description: 'hire/work: agent profile name (alt to positional arg)' },
|
|
4376
4499
|
rank: { type: 'string', description: 'hire: agent rank (principal|senior|junior|decider)' },
|
|
4377
4500
|
command: { type: 'string', description: 'hire: CLI command that runs the agent harness (e.g. copilot, claude, pi)' },
|
|
4501
|
+
arg: { type: 'string', multiple: true, description: 'hire/work: command-line switch/arg appended to the harness command (repeatable), e.g. --arg --allow-all. Persisted on hire; work appends more.' },
|
|
4378
4502
|
model: { type: 'string', description: 'hire: model name passed to the harness (AGENT_MODEL)' },
|
|
4379
4503
|
capabilities: { type: 'string', description: 'hire: comma-separated capability list' },
|
|
4380
4504
|
sandbox: { type: 'string', description: 'hire/work: execution sandbox none|docker|podman (default none). Containers isolate each job.' },
|
|
@@ -4390,6 +4514,7 @@ export const commands = {
|
|
|
4390
4514
|
list: { type: 'boolean', description: 'hire: list existing agent profiles instead of creating one' },
|
|
4391
4515
|
'max-parallel': { type: 'string', description: 'work: max concurrent jobs per worker (default 1)' },
|
|
4392
4516
|
'job-timeout': { type: 'string', description: 'work: max harness runtime per job in ms; the spawned process is killed past this (default 300000)' },
|
|
4517
|
+
'lock-grace': { type: 'string', description: 'work: extra ms added to --job-timeout to derive the broker activation lock, so the worker reports before the lock lapses (default 120000)' },
|
|
4393
4518
|
'job-type': { type: 'string', multiple: true, description: 'work: extra job type to service alongside the rank×capability matrix (repeatable)' },
|
|
4394
4519
|
},
|
|
4395
4520
|
handler: async (args, flags) => {
|
|
@@ -4533,8 +4658,8 @@ function printUsage() {
|
|
|
4533
4658
|
console.log(' c8ctl nano set <bin|model-dir> <path>');
|
|
4534
4659
|
console.log(' c8ctl nano config');
|
|
4535
4660
|
console.log(' c8ctl nano update [--check]');
|
|
4536
|
-
console.log(' c8ctl nano hire [--name <n>] [--rank <r>] [--command <c>] [--model <m>] [--capabilities <a,b>] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--list]');
|
|
4537
|
-
console.log(' c8ctl nano work <profileName> [--max-parallel <n>] [--job-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]');
|
|
4661
|
+
console.log(' c8ctl nano hire [--name <n>] [--rank <r>] [--command <c>] [--arg <switch> ...] [--model <m>] [--capabilities <a,b>] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--list]');
|
|
4662
|
+
console.log(' c8ctl nano work <profileName> [--arg <switch> ...] [--max-parallel <n>] [--job-timeout <ms>] [--lock-grace <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]');
|
|
4538
4663
|
console.log('');
|
|
4539
4664
|
console.log('Subcommands:');
|
|
4540
4665
|
console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
|
|
@@ -4577,6 +4702,7 @@ function printUsage() {
|
|
|
4577
4702
|
console.log(' --max-parallel <n> work: max concurrent jobs per worker (default 1)');
|
|
4578
4703
|
console.log(' --job-type <token> work: extra job type to service alongside the rank×capability matrix (repeatable)');
|
|
4579
4704
|
console.log(' --job-timeout <ms> work: max harness runtime per job in ms (default 300000)');
|
|
4705
|
+
console.log(' --lock-grace <ms> work: extra ms over --job-timeout for the broker activation lock (default 120000)');
|
|
4580
4706
|
console.log(' --secret-resolver <r> work: secret resolver for task secretRefs (host; default host)');
|
|
4581
4707
|
console.log(' --reap-age <ms> work: age before a finished agent container/workspace is reaped (default 3600000)');
|
|
4582
4708
|
console.log(' --reap-interval <ms> work: how often to sweep finished agent containers/workspaces (default 300000)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.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",
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"semantic-release": "^25.0.3"
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.
|
|
53
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
54
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
55
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
56
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
57
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
58
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
52
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.18.1",
|
|
53
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.18.1",
|
|
54
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.18.1",
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.18.1",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.18.1",
|
|
57
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.18.1",
|
|
58
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.18.1"
|
|
59
59
|
}
|
|
60
60
|
}
|