c8ctl-plugin-nano 1.39.2 → 1.40.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.
Files changed (2) hide show
  1. package/c8ctl-plugin.js +99 -3
  2. package/package.json +8 -8
package/c8ctl-plugin.js CHANGED
@@ -1549,6 +1549,33 @@ const RANKS = ['principal', 'senior', 'junior', 'decider'];
1549
1549
  // opt-in per role because a TTY changes the harness's I/O semantics.
1550
1550
  const TERMINAL_MODES = ['pipe', 'pty'];
1551
1551
 
1552
+ // #110: the harness protocol a role drives its agent over — a plain stdin/scrape
1553
+ // `pipe` (the default floor) or `acp` (Agent Client Protocol, JSON-RPC over
1554
+ // stdio). Default is `pipe`; `acp` is opt-in per role. The ACP executor lands in
1555
+ // a downstream task — this seam only carries the schema/plumbing.
1556
+ const PROTOCOLS = ['pipe', 'acp'];
1557
+
1558
+ // #110: the ACP permission policy for a role. Only `yolo` (auto-allow-all) is
1559
+ // enforced today; `escalate`/`filter` are RESERVED pending nano-workforce#559
1560
+ // (the permission-event + escalation bridge) and are not yet enforced — they are
1561
+ // accepted and persisted for forward-compatibility (never downgraded), but today
1562
+ // effectively behave like `yolo` (auto-allow). Default is `yolo`.
1563
+ const PERMISSION_MODES = ['yolo', 'escalate', 'filter'];
1564
+
1565
+ // #110: resolve a role's agentic setting (protocol/permission) with a uniform
1566
+ // env-override → profile → default precedence, tolerating invalid values at
1567
+ // every layer. A one-off worker env var wins if it names an allowed value; else
1568
+ // the persisted hire profile decides if it holds an allowed value; else the safe
1569
+ // default. Reserved-but-allowed values (e.g. escalate/filter) carry through
1570
+ // verbatim; unknown values are ignored and fall through to the next layer.
1571
+ function resolveAgenticSetting(envValue, profileValue, allowed, dflt) {
1572
+ const env = String(envValue || '').trim().toLowerCase();
1573
+ if (allowed.includes(env)) return env;
1574
+ const profile = String(profileValue || '').trim().toLowerCase();
1575
+ if (allowed.includes(profile)) return profile;
1576
+ return dflt;
1577
+ }
1578
+
1552
1579
  /** Normalize a capability list: trim, drop empties, de-dupe, sort (canonical). */
1553
1580
  function normalizeCapabilities(input) {
1554
1581
  const raw = Array.isArray(input)
@@ -1771,6 +1798,15 @@ function normalizeStoredProfile(name, profile) {
1771
1798
  // to the safe `pipe` default rather than failing the whole profile.
1772
1799
  const terminalRaw = typeof profile.terminal === 'string' ? profile.terminal.trim().toLowerCase() : '';
1773
1800
  const terminal = TERMINAL_MODES.includes(terminalRaw) ? terminalRaw : 'pipe';
1801
+ // #110: harness protocol + ACP permission policy. Tolerant like `terminal` —
1802
+ // an unknown/legacy/missing value falls back to the safe defaults ('pipe' /
1803
+ // 'yolo') rather than failing the whole profile. A persisted escalate/filter
1804
+ // is preserved verbatim (it is enforced by a downstream task pending
1805
+ // nano-workforce#559), never downgraded.
1806
+ const protocolRaw = typeof profile.protocol === 'string' ? profile.protocol.trim().toLowerCase() : '';
1807
+ const protocol = PROTOCOLS.includes(protocolRaw) ? protocolRaw : 'pipe';
1808
+ const permissionRaw = typeof profile.permission === 'string' ? profile.permission.trim().toLowerCase() : '';
1809
+ const permission = PERMISSION_MODES.includes(permissionRaw) ? permissionRaw : 'yolo';
1774
1810
  return {
1775
1811
  profile: {
1776
1812
  name,
@@ -1782,6 +1818,8 @@ function normalizeStoredProfile(name, profile) {
1782
1818
  sandbox,
1783
1819
  image,
1784
1820
  terminal,
1821
+ protocol,
1822
+ permission,
1785
1823
  env: normalizeEnvMap(profile.env),
1786
1824
  },
1787
1825
  };
@@ -1900,7 +1938,15 @@ async function hireWorker(req, flags) {
1900
1938
  for (const name of names.sort()) {
1901
1939
  const p = hires[name];
1902
1940
  const term = String(p.terminal || '').trim().toLowerCase() === 'pty' ? '; terminal: pty' : '';
1903
- logger.info(` ${name} [${p.rank}] ${buildAgentCommandLine(p.command, p.args)} (model: ${p.model || '-'}; caps: ${normalizeCapabilities(p.capabilities).join(', ') || '-'}${term})`);
1941
+ const proto = String(p.protocol || '').trim().toLowerCase() === 'acp' ? '; protocol: acp' : '';
1942
+ const perm = (() => {
1943
+ const v = String(p.permission || '').trim().toLowerCase();
1944
+ // Only surface recognized non-default modes; normalizeStoredProfile
1945
+ // coerces unknown/legacy values back to yolo at runtime, so showing them
1946
+ // here would make --list disagree with actual behavior.
1947
+ return v && v !== 'yolo' && PERMISSION_MODES.includes(v) ? `; permission: ${v}` : '';
1948
+ })();
1949
+ logger.info(` ${name} [${p.rank}] ${buildAgentCommandLine(p.command, p.args)} (model: ${p.model || '-'}; caps: ${normalizeCapabilities(p.capabilities).join(', ') || '-'}${term}${proto}${perm})`);
1904
1950
  }
1905
1951
  logger.info('');
1906
1952
  logger.info('Put one to work with: c8ctl nano work <name>');
@@ -1917,6 +1963,8 @@ async function hireWorker(req, flags) {
1917
1963
  let sandbox = flags?.sandbox !== undefined ? String(flags.sandbox).trim().toLowerCase() : undefined;
1918
1964
  let image = flags?.image !== undefined ? String(flags.image).trim() : undefined;
1919
1965
  let terminal = flags?.terminal !== undefined ? String(flags.terminal).trim().toLowerCase() : undefined;
1966
+ let protocol = flags?.protocol !== undefined ? String(flags.protocol).trim().toLowerCase() : undefined;
1967
+ let permission = flags?.permission !== undefined ? String(flags.permission).trim().toLowerCase() : undefined;
1920
1968
  // Structured command-line switches appended to the command when spawned, e.g.
1921
1969
  // `--arg --allow-all` for `copilot`. Repeatable; each --arg is one argv token.
1922
1970
  const commandArgs = normalizeArgList(flags?.arg);
@@ -1995,6 +2043,8 @@ async function hireWorker(req, flags) {
1995
2043
  if (sandbox === undefined || sandbox === '') sandbox = 'none';
1996
2044
  if (image === undefined) image = '';
1997
2045
  if (terminal === undefined || terminal === '') terminal = 'pipe';
2046
+ if (protocol === undefined || protocol === '') protocol = 'pipe';
2047
+ if (permission === undefined || permission === '') permission = 'yolo';
1998
2048
 
1999
2049
  if (!SANDBOXES.includes(sandbox)) {
2000
2050
  logger.error(`Invalid --sandbox "${sandbox}". Use one of: ${SANDBOXES.join(', ')}`);
@@ -2004,6 +2054,21 @@ async function hireWorker(req, flags) {
2004
2054
  logger.error(`Invalid --terminal "${terminal}". Use one of: ${TERMINAL_MODES.join(', ')}`);
2005
2055
  process.exit(1);
2006
2056
  }
2057
+ if (!PROTOCOLS.includes(protocol)) {
2058
+ logger.error(`Invalid --protocol "${protocol}". Use one of: ${PROTOCOLS.join(', ')}`);
2059
+ process.exit(1);
2060
+ }
2061
+ if (!PERMISSION_MODES.includes(permission)) {
2062
+ logger.error(`Invalid --permission "${permission}". Use one of: ${PERMISSION_MODES.join(', ')}`);
2063
+ process.exit(1);
2064
+ }
2065
+ // #110: escalate/filter are accepted and persisted for forward-compatibility,
2066
+ // but not yet enforced (pending nano-workforce#559). Warn the operator so a
2067
+ // hire is never misread as gating destructive ops today — the value is kept as
2068
+ // given (never downgraded to yolo).
2069
+ if (permission === 'escalate' || permission === 'filter') {
2070
+ logger.warn(`Permission policy "${permission}" is RESERVED and NOT enforced in this build (pending nano-workforce#559): it does not gate anything today and effectively behaves like yolo (auto-allow all permission requests). The value is persisted as-is for forward-compatibility.`);
2071
+ }
2007
2072
 
2008
2073
  if (CONTAINER_SANDBOXES.has(sandbox) && !image) {
2009
2074
  logger.error(`--sandbox ${sandbox} requires --image <ref> (the container image the agent runs in).`);
@@ -2034,6 +2099,8 @@ async function hireWorker(req, flags) {
2034
2099
  sandbox,
2035
2100
  image: image || '',
2036
2101
  terminal,
2102
+ protocol,
2103
+ permission,
2037
2104
  env: profileEnv,
2038
2105
  createdAt: new Date().toISOString(),
2039
2106
  };
@@ -2046,6 +2113,8 @@ async function hireWorker(req, flags) {
2046
2113
  if (profile.args.length > 0) logger.info(` args: ${profile.args.map(shQuote).join(' ')}`);
2047
2114
  logger.info(` sandbox: ${profile.sandbox}${CONTAINER_SANDBOXES.has(profile.sandbox) ? ` (image ${profile.image})` : ''}`);
2048
2115
  logger.info(` live terminal: ${profile.terminal}${profile.terminal === 'pty' ? ' (streamed + steerable on the relay lane)' : ''}`);
2116
+ logger.info(` protocol: ${profile.protocol}${profile.protocol === 'acp' ? ' (Agent Client Protocol — JSON-RPC over stdio; RESERVED — accepted/persisted but not yet active in this build; the harness still runs on the transport selected by --terminal (pipe or pty))' : ''}`);
2117
+ logger.info(` permission: ${profile.permission}${(profile.permission === 'escalate' || profile.permission === 'filter') ? ' (RESERVED — not yet enforced, pending nano-workforce#559)' : ''}`);
2049
2118
  const envKeys = Object.keys(profile.env);
2050
2119
  if (envKeys.length > 0) logger.info(` env: ${envKeys.join(', ')}`);
2051
2120
  logger.info(` job types (${matrix.length}): ${matrix.join(' ')}`);
@@ -3969,7 +4038,12 @@ function startLockExtender(job, windowMs, intervalMs, tag, logger) {
3969
4038
  * Both paths resolve to the same result contract.
3970
4039
  */
3971
4040
  function runAgentJob(profile, job, opts = {}) {
3972
- const { timeoutMs, idleTimeoutMs, envelope, sandbox = 'none', image, runId, secretEnv = {}, passThroughSecretNames = [], cwd, extraEnv = {}, profileEnv = {}, resultFile, stream = false, streamPrefix = '', onStreamOut, onStreamErr, args: commandArgs, terminal = 'pipe', relaySession = null, ptyFactory } = opts;
4041
+ const { timeoutMs, idleTimeoutMs, envelope, sandbox = 'none', image, runId, secretEnv = {}, passThroughSecretNames = [], cwd, extraEnv = {}, profileEnv = {}, resultFile, stream = false, streamPrefix = '', onStreamOut, onStreamErr, args: commandArgs, terminal = 'pipe', protocol = 'pipe', permission = 'yolo', relaySession = null, ptyFactory } = opts;
4042
+ // #110: `protocol`/`permission` are accepted here so the seam exists for the
4043
+ // downstream ACP executor. This task does NOT dispatch on them — the pipe/PTY
4044
+ // paths below are unchanged, so `protocol === 'pipe'` behavior is identical.
4045
+ // TODO(#110): the acp dispatch branch (spawnCaptureAcp) lands in a later task.
4046
+ void protocol; void permission;
3973
4047
  const payload = JSON.stringify(buildAgentPayload(profile, job, envelope));
3974
4048
  const agentEnv = baseAgentEnv(profile, job);
3975
4049
  // The harness command line: the profile command plus its structured switches
@@ -5069,6 +5143,17 @@ async function workAgent(req, flags) {
5069
5143
  logger.info(` live terminal: ${roleTerminal === 'pty' ? 'PTY (streamed + steerable)' : 'pipe (streamed)'} on the relay lane.`);
5070
5144
  }
5071
5145
 
5146
+ // #110: the role's harness protocol (pipe|acp) and ACP permission policy
5147
+ // (yolo|escalate|filter), resolved with the same env-override-then-profile
5148
+ // precedence as terminal. `NANO_AGENTIC_PROTOCOL`/`NANO_AGENTIC_PERMISSION`
5149
+ // override a one-off worker; otherwise the hire profile decides; else the safe
5150
+ // defaults (pipe/yolo). escalate/filter are carried through verbatim — the
5151
+ // acp-executor task enforces yolo and interim-handles the reserved policies.
5152
+ const envProtocol = (process.env.NANO_AGENTIC_PROTOCOL || '').trim().toLowerCase();
5153
+ const roleProtocol = resolveAgenticSetting(envProtocol, profile.protocol, PROTOCOLS, 'pipe');
5154
+ const envPermission = (process.env.NANO_AGENTIC_PERMISSION || '').trim().toLowerCase();
5155
+ const rolePermission = resolveAgenticSetting(envPermission, profile.permission, PERMISSION_MODES, 'yolo');
5156
+
5072
5157
  // A per-job-type worker factory. Captures all the CLI-local + profile context
5073
5158
  // in closure scope so the profile watcher below can (re)spawn a poller for any
5074
5159
  // job type on demand without re-reading the flags.
@@ -5254,6 +5339,11 @@ async function workAgent(req, flags) {
5254
5339
  // stream on the relay lane when a relay session exists (skipped when
5255
5340
  // relaySession is null); only a PTY is interactively steerable.
5256
5341
  terminal: roleTerminal,
5342
+ // #110: harness protocol + ACP permission policy threaded to
5343
+ // runAgentJob. Inert in this seam task (pipe/pty dispatch unchanged);
5344
+ // the acp-executor task acts on them.
5345
+ protocol: roleProtocol,
5346
+ permission: rolePermission,
5257
5347
  relaySession,
5258
5348
  // Route the --stream tee through c8ctl's output-mode-aware logger so
5259
5349
  // spying never corrupts a structured/JSON output mode.
@@ -8940,6 +9030,7 @@ export {
8940
9030
  export { setConfig, unsetConfig, readConfig, writeConfig, getConfigFile, SETTING_ALIASES };
8941
9031
  export { buildNpmInvocation };
8942
9032
  export { resolveAgenticConfig, LOCAL_AGENTIC_TOKEN };
9033
+ export { resolveAgenticSetting, PROTOCOLS, PERMISSION_MODES };
8943
9034
  export { resolveAgenticTarget, discoverAgenticHubs, probeAgenticChannel, normalizeProjectApps, isLoopbackHost };
8944
9035
  export { compareSemver, githubRepoSlug, filterReleasesSince, renderReleaseBody };
8945
9036
  export {
@@ -9103,6 +9194,7 @@ export const metadata = {
9103
9194
  { command: 'c8ctl nano hire --list', description: 'List hired agent profiles' },
9104
9195
  { 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' },
9105
9196
  { command: 'c8ctl nano hire --name coder --rank senior --command copilot --terminal pty', description: 'Opt this role into a full, steerable live terminal (PTY) streamed on the agentic relay lane (default: pipe)' },
9197
+ { command: 'c8ctl nano hire --name coder --rank senior --command copilot --protocol acp --permission yolo', description: 'Accept/persist ACP (JSON-RPC/stdio) for this role — RESERVED, not yet active in this build (acp is inert; the harness still runs on the transport selected by --terminal, pipe or pty); escalate/filter permission modes are likewise reserved/not yet active' },
9106
9198
  { command: 'c8ctl nano assign reviewer code-review,testing', description: 'Grant more capabilities (comma-separated, like hire) to an existing hire — additive; running workers hot-reload it' },
9107
9199
  { command: 'c8ctl nano work reviewer', description: 'Spawn Nano job workers for the "reviewer" profile and poll for work' },
9108
9200
  { command: 'c8ctl nano work coder --auto', description: 'Zero-config: serve every deployed agent job type read straight from the engine — no capability, no wiring (great for a local single-tenant plane)' },
@@ -9165,6 +9257,8 @@ export const commands = {
9165
9257
  sandbox: { type: 'string', description: 'hire/work: execution sandbox none|docker|podman (default none). Containers isolate each job.' },
9166
9258
  image: { type: 'string', description: 'hire/work: container image the agent runs in (required for --sandbox docker|podman)' },
9167
9259
  terminal: { type: 'string', description: 'hire: live-terminal mode for this role — pty (full terminal, streamed + steerable on the relay lane) or pipe (default). NANO_AGENTIC_TERMINAL overrides at work time.' },
9260
+ protocol: { type: 'string', description: 'hire: harness protocol pipe|acp (default pipe). acp is RESERVED — accepted and persisted for forward-compatibility but not yet implemented in this build (inert: the harness still runs on the transport selected by --terminal, pipe or pty; the ACP JSON-RPC-over-stdio executor lands downstream). NANO_AGENTIC_PROTOCOL overrides at work time.' },
9261
+ permission: { type: 'string', description: 'hire: ACP permission policy (default yolo). yolo auto-allows all permission requests. escalate|filter are RESERVED/not-yet-active in this build (pending nano-workforce#559): they are persisted but not enforced and effectively behave like yolo (auto-allow). NANO_AGENTIC_PERMISSION overrides at work time.' },
9168
9262
  env: { type: 'string', multiple: true, description: 'hire/work: static env var for the harness as NAME=VALUE (repeatable); persisted on hire, work extends/overrides. E.g. permission toggles.' },
9169
9263
  'secret-resolver': { type: 'string', description: 'work: secret resolver for task secretRefs (host = process env; default host)' },
9170
9264
  'reap-age': { type: 'string', description: 'work: age in ms before a finished agent container or job workspace is reaped (default 3600000)' },
@@ -9337,7 +9431,7 @@ function printUsage() {
9337
9431
  console.log(' c8ctl nano unset <bin|model-dir>');
9338
9432
  console.log(' c8ctl nano config');
9339
9433
  console.log(' c8ctl nano update [--check]');
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]');
9434
+ 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] [--protocol pipe|acp] [--permission yolo|escalate|filter] [--env NAME=VALUE ...] [--list]');
9341
9435
  console.log(' c8ctl nano assign <profileName> <cap[,cap...]> [--name <n>] [--capabilities <a,b>]');
9342
9436
  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]');
9343
9437
  console.log(' c8ctl nano supervisor [start|status|add|remove|restart|stop|logs|attach] ... (manage many workers from one terminal)');
@@ -9382,6 +9476,8 @@ function printUsage() {
9382
9476
  console.log(' --sandbox <s> hire/work: execution sandbox none|docker|podman (default none)');
9383
9477
  console.log(' --image <ref> hire/work: container image the agent runs in (required for docker|podman)');
9384
9478
  console.log(' --terminal <m> hire: live-terminal mode pty|pipe (default pipe); pty streams a steerable terminal on the relay lane');
9479
+ console.log(' --protocol <p> hire: harness protocol pipe|acp (default pipe); acp is RESERVED — accepted/persisted but not yet implemented in this build (inert: the harness still runs on the transport selected by --terminal, pipe or pty). NANO_AGENTIC_PROTOCOL overrides at work time');
9480
+ console.log(' --permission <p> hire: ACP permission policy yolo|escalate|filter (default yolo); yolo auto-allows all requests. escalate|filter are RESERVED/not-yet-active (pending nano-workforce#559) — persisted but not enforced, effectively behave like yolo (auto-allow). NANO_AGENTIC_PERMISSION overrides at work time');
9385
9481
  console.log(' --env NAME=VALUE hire/work: static env var for the harness (repeatable); persisted on hire, work extends/overrides');
9386
9482
  console.log(' --list hire: list existing agent profiles instead of creating one');
9387
9483
  console.log(' --job-type <token> work: extra job type to service alongside the rank×capability matrix (repeatable)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.39.2",
3
+ "version": "1.40.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",
@@ -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.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"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.40.0",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.40.0",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.40.0",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.40.0",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.40.0",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.40.0",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.40.0"
67
67
  }
68
68
  }