quoxcli 0.4.0 → 0.6.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 (199) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/admin.js +7 -0
  6. package/lib/api/endpoints/agent-attach.js +7 -0
  7. package/lib/api/endpoints/compliance.js +25 -0
  8. package/lib/api/endpoints/governance.js +7 -0
  9. package/lib/api/endpoints/inbox.js +6 -0
  10. package/lib/api/endpoints/inboxq.js +58 -0
  11. package/lib/api/endpoints/index.js +4 -0
  12. package/lib/api/endpoints/lucid.js +17 -0
  13. package/lib/api/endpoints/mcp.js +35 -0
  14. package/lib/api/endpoints/mon.js +122 -0
  15. package/lib/api/endpoints/notesq.js +2 -0
  16. package/lib/api/endpoints/quoxplan.js +51 -0
  17. package/lib/api/endpoints/tools.js +13 -0
  18. package/lib/api/endpoints/v1.js +13 -0
  19. package/lib/commands/actions.js +51 -13
  20. package/lib/commands/admin.js +35 -0
  21. package/lib/commands/agentAttach.js +15 -0
  22. package/lib/commands/approvals.js +193 -3
  23. package/lib/commands/ask.js +1 -1
  24. package/lib/commands/auth.js +64 -11
  25. package/lib/commands/backup.js +11 -4
  26. package/lib/commands/capabilities.js +3 -2
  27. package/lib/commands/chat.js +15 -0
  28. package/lib/commands/claude.js +4 -1
  29. package/lib/commands/compliance.js +109 -0
  30. package/lib/commands/connect.js +69 -0
  31. package/lib/commands/contacts.js +2 -0
  32. package/lib/commands/dev.js +6 -8
  33. package/lib/commands/doctor.js +22 -3
  34. package/lib/commands/evidence.js +2 -1
  35. package/lib/commands/gripe.js +24 -13
  36. package/lib/commands/inbox.js +37 -3
  37. package/lib/commands/inboxq.js +147 -0
  38. package/lib/commands/kuma.js +78 -5
  39. package/lib/commands/licenceLinkView.js +311 -0
  40. package/lib/commands/linkedin.js +19 -0
  41. package/lib/commands/loop.js +30 -3
  42. package/lib/commands/lucid.js +7 -0
  43. package/lib/commands/lucidGoals.js +389 -0
  44. package/lib/commands/mail.js +427 -25
  45. package/lib/commands/mcp.js +190 -0
  46. package/lib/commands/memory.js +68 -4
  47. package/lib/commands/memorySync.js +158 -0
  48. package/lib/commands/mon.js +69 -144
  49. package/lib/commands/monitoring.js +68 -0
  50. package/lib/commands/notes.js +29 -7
  51. package/lib/commands/notesSeal.js +617 -0
  52. package/lib/commands/objective.js +5 -2
  53. package/lib/commands/offload.js +52 -12
  54. package/lib/commands/plan.js +487 -52
  55. package/lib/commands/provider.js +29 -8
  56. package/lib/commands/qlarity.js +32 -1
  57. package/lib/commands/run.js +2 -1
  58. package/lib/commands/server.js +37 -0
  59. package/lib/commands/setup.js +127 -75
  60. package/lib/commands/skills.js +4 -1
  61. package/lib/commands/slot.js +49 -9
  62. package/lib/commands/status.js +92 -44
  63. package/lib/commands/tool.js +30 -0
  64. package/lib/commands/vault.js +6 -3
  65. package/lib/commands/volt.js +6 -1
  66. package/lib/commands/walkthrough.js +14 -2
  67. package/lib/commands/watch.js +11 -2
  68. package/lib/commands/wazuh.js +880 -0
  69. package/lib/commands/wazuhIntake.js +358 -0
  70. package/lib/commands/wazuhReport.js +192 -0
  71. package/lib/commands/workflow.js +47 -4
  72. package/lib/commands/x.js +50 -0
  73. package/lib/core/announce.js +86 -0
  74. package/lib/core/auth.js +20 -2
  75. package/lib/core/callMe.js +118 -0
  76. package/lib/core/client.js +27 -1
  77. package/lib/core/commandRegistrars.js +9 -0
  78. package/lib/core/config.js +42 -3
  79. package/lib/core/confirm.js +68 -7
  80. package/lib/core/crossOrgApprovals.js +69 -0
  81. package/lib/core/errors.js +52 -1
  82. package/lib/core/governedSession.js +135 -5
  83. package/lib/core/inboxResolution.js +3 -2
  84. package/lib/core/loginGuard.js +73 -0
  85. package/lib/core/offload.js +120 -0
  86. package/lib/core/org-override.js +5 -1
  87. package/lib/core/output.js +30 -5
  88. package/lib/core/profileTrust.js +182 -0
  89. package/lib/core/quoxglyph.js +22 -1
  90. package/lib/core/router.js +14 -0
  91. package/lib/core/selfUpdate.js +2 -0
  92. package/lib/core/stepUpPrompt.js +99 -0
  93. package/lib/core/version.js +1 -1
  94. package/lib/core/walkthrough.js +9 -5
  95. package/lib/dragon/select.js +2 -2
  96. package/lib/quoxon/EXTENSIONS.md +142 -19
  97. package/lib/quoxon/api.js +189 -34
  98. package/lib/quoxon/app.js +1277 -188
  99. package/lib/quoxon/approvalBatch.js +117 -0
  100. package/lib/quoxon/approvalFacts.js +56 -2
  101. package/lib/quoxon/approvalSentence.js +8 -4
  102. package/lib/quoxon/approvalsView.js +72 -12
  103. package/lib/quoxon/ask.js +4 -2
  104. package/lib/quoxon/autoOpen.js +94 -0
  105. package/lib/quoxon/backgroundWork.js +59 -0
  106. package/lib/quoxon/bang.js +8 -2
  107. package/lib/quoxon/capabilities.js +7 -2
  108. package/lib/quoxon/commandShort.js +8 -1
  109. package/lib/quoxon/commands.js +42 -12
  110. package/lib/quoxon/connect.js +5 -3
  111. package/lib/quoxon/deckLazy.js +58 -0
  112. package/lib/quoxon/delegationDragon.js +49 -0
  113. package/lib/quoxon/destructive.js +35 -0
  114. package/lib/quoxon/dnd.js +118 -0
  115. package/lib/quoxon/dragonCompanion.js +37 -7
  116. package/lib/quoxon/dragonMoments.js +78 -0
  117. package/lib/quoxon/echo.js +1 -0
  118. package/lib/quoxon/events.js +55 -8
  119. package/lib/quoxon/extensions.js +400 -38
  120. package/lib/quoxon/foreignText.js +49 -0
  121. package/lib/quoxon/gripeQueue.js +135 -0
  122. package/lib/quoxon/inboxView.js +125 -22
  123. package/lib/quoxon/keys.js +2 -2
  124. package/lib/quoxon/launcher.js +2 -1
  125. package/lib/quoxon/loopCard.js +7 -2
  126. package/lib/quoxon/loopCards.js +17 -8
  127. package/lib/quoxon/loopStream.js +20 -9
  128. package/lib/quoxon/mailView.js +71 -0
  129. package/lib/quoxon/main.js +27 -6
  130. package/lib/quoxon/markdown.js +1 -1
  131. package/lib/quoxon/model.js +15 -0
  132. package/lib/quoxon/neverAnswered.js +20 -0
  133. package/lib/quoxon/objects/away.js +43 -5
  134. package/lib/quoxon/onboarding/connectServer.js +162 -0
  135. package/lib/quoxon/onboarding/copy.js +6 -0
  136. package/lib/quoxon/onboarding/dockerOffer.js +3 -1
  137. package/lib/quoxon/onboarding/index.js +5 -43
  138. package/lib/quoxon/onboarding/probe.js +20 -1
  139. package/lib/quoxon/onboarding/view.js +16 -3
  140. package/lib/quoxon/orgSwitch.js +22 -0
  141. package/lib/quoxon/print.js +2 -0
  142. package/lib/quoxon/profileSwitch.js +7 -0
  143. package/lib/quoxon/promptHistory.js +3 -1
  144. package/lib/quoxon/render.js +68 -16
  145. package/lib/quoxon/resend.js +14 -0
  146. package/lib/quoxon/roster.js +10 -5
  147. package/lib/quoxon/rosterSessions.js +44 -0
  148. package/lib/quoxon/rows.js +3 -1
  149. package/lib/quoxon/screenContext.js +1 -1
  150. package/lib/quoxon/secretShape.js +88 -0
  151. package/lib/quoxon/slash/agents.js +2 -1
  152. package/lib/quoxon/slash/argSources.js +9 -2
  153. package/lib/quoxon/slash/args.js +4 -2
  154. package/lib/quoxon/slash/broadcast.js +48 -11
  155. package/lib/quoxon/slash/broadcastRun.js +24 -7
  156. package/lib/quoxon/slash/cliLeaves.json +98 -1
  157. package/lib/quoxon/slash/dashboard.js +2 -0
  158. package/lib/quoxon/slash/hatWalk.js +1 -1
  159. package/lib/quoxon/slash/listView.js +2 -1
  160. package/lib/quoxon/slash/palette.js +63 -1
  161. package/lib/quoxon/slash/paletteController.js +15 -5
  162. package/lib/quoxon/slash/restart.js +9 -4
  163. package/lib/quoxon/slash/stepUp.js +5 -5
  164. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  165. package/lib/quoxon/slashSpinner.js +35 -0
  166. package/lib/quoxon/slotAttach.js +8 -0
  167. package/lib/quoxon/slotChat.js +24 -3
  168. package/lib/quoxon/slotFormat.js +1 -1
  169. package/lib/quoxon/slotOutbox.js +113 -0
  170. package/lib/quoxon/slotRun.js +2 -0
  171. package/lib/quoxon/suggestCommand.js +1 -1
  172. package/lib/quoxon/term.js +25 -1
  173. package/lib/quoxon/transcript.js +2 -1
  174. package/lib/quoxon/webPty.js +15 -0
  175. package/lib/quoxon/welcome.js +1 -1
  176. package/lib/quoxon/whatsnew.js +1 -1
  177. package/lib/quoxon/whatsnew.jsonl +42 -0
  178. package/lib/render/planBrief.js +614 -0
  179. package/lib/server/installFlow.js +168 -0
  180. package/lib/server/status.js +15 -1
  181. package/lib/toolOutputShape.js +61 -3
  182. package/package.json +3 -2
  183. package/vendor/dragonlab/SOURCE +3 -3
  184. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  185. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  186. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  187. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  188. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  189. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  190. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  191. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  192. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  193. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  194. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  195. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  196. package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
  197. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  198. package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
  199. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
@@ -25,6 +25,7 @@
25
25
  * None of the three are org-scoped (single shared config/cache).
26
26
  */
27
27
 
28
+ import { attachWazuhVerbs } from './wazuh.js';
28
29
  import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
29
30
  import { parseJSON } from './helpers.js';
30
31
  import { resolveEndpoint } from '../api/endpoints.js';
@@ -77,6 +78,17 @@ function reportDockerReadFailures(data) {
77
78
  return false;
78
79
  }
79
80
 
81
+ /**
82
+ * AGH-chain reads answer 202 { status: 'computing' } while the collector builds them in the
83
+ * background; an empty body then is not data. Says so and returns true (caller stops).
84
+ */
85
+ function reportComputing(program, data, message) {
86
+ if (!data || data.status !== 'computing') return false;
87
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return true; }
88
+ printWarning(message);
89
+ return true;
90
+ }
91
+
80
92
  export function registerMonCommands(program, ctx) {
81
93
  const { client, auth } = ctx;
82
94
 
@@ -262,147 +274,48 @@ export function registerMonCommands(program, ctx) {
262
274
  .command('wazuh')
263
275
  .description('Wazuh security monitoring: manager status, agents, alerts, vulnerabilities');
264
276
 
265
- wazuh
266
- .command('status')
267
- .description('Show Wazuh manager status')
268
- .action(async () => {
269
- auth.requireAuth();
270
- const ep = resolveEndpoint('mon.wazuh.manager-status.get');
271
- const data = await client.get(ep.path, { service: ep.service });
272
- formatDetail(data.data || data, { format: program.opts().output, title: 'Wazuh Manager Status' });
273
- });
274
-
275
- wazuh
276
- .command('agents')
277
- .description('List Wazuh agents')
278
- .option('--status <status>', 'filter by agent status')
279
- .option('--limit <n>', 'max results')
280
- .option('--offset <n>', 'result offset')
281
- .option('--q <query>', 'query string passed through to the Wazuh API')
282
- .action(async (options) => {
283
- auth.requireAuth();
284
- const ep = resolveEndpoint('mon.wazuh.agents.list');
285
- const query = {};
286
- if (options.status) query.status = options.status;
287
- if (options.limit) query.limit = options.limit;
288
- if (options.offset) query.offset = options.offset;
289
- if (options.q) query.q = options.q;
290
- const data = await client.get(ep.path, { service: ep.service, query });
291
- const agents = data.data?.data?.affected_items || data.data?.affected_items || data.data || [];
292
- formatOutput({
293
- columns: ['id', 'name', 'ip', 'status', 'version'],
294
- headers: ['ID', 'Name', 'IP', 'Status', 'Version'],
295
- rows: Array.isArray(agents) ? agents : [],
296
- }, { format: program.opts().output, emptyMessage: 'No Wazuh agents found.' });
297
- });
298
-
299
- wazuh
300
- .command('alerts')
301
- .description('Query recent Wazuh alerts')
302
- .option('--q <query>', 'search text (rule description / agent name / rule groups)')
303
- .option('--limit <n>', 'max results (default 50, capped at 500)')
304
- .action(async (options) => {
305
- auth.requireAuth();
306
- const ep = resolveEndpoint('mon.wazuh.alerts.list');
307
- const query = {};
308
- if (options.q) query.q = options.q;
309
- if (options.limit) query.limit = options.limit;
310
- const data = await client.get(ep.path, { service: ep.service, query });
311
- formatOutput({
312
- columns: ['timestamp', 'rule', 'agent', 'level'],
313
- headers: ['Time', 'Rule', 'Agent', 'Level'],
314
- rows: (data.data || []).map(a => ({
315
- timestamp: a.timestamp,
316
- rule: a.rule?.description || '',
317
- agent: a.agent?.name || '',
318
- level: a.rule?.level ?? '',
319
- })),
320
- }, { format: program.opts().output, emptyMessage: 'No alerts found.' });
321
- });
322
-
323
- wazuh
324
- .command('vulns')
325
- .description('Query current vulnerability states')
326
- .option('--agent-id <id>', 'filter by agent ID')
327
- .option('--agent-name <name>', 'filter by agent name')
328
- .option('--severity <severity>', 'filter by severity')
329
- .option('--limit <n>', 'max results (default 100, capped at 1000)')
330
- .action(async (options) => {
331
- auth.requireAuth();
332
- const ep = resolveEndpoint('mon.wazuh.vulnerabilities.list');
333
- const query = {};
334
- if (options.agentId) query.agent_id = options.agentId;
335
- if (options.agentName) query.agent_name = options.agentName;
336
- if (options.severity) query.severity = options.severity;
337
- if (options.limit) query.limit = options.limit;
338
- const data = await client.get(ep.path, { service: ep.service, query });
339
- formatOutput({
340
- columns: ['agent', 'cve', 'severity', 'score'],
341
- headers: ['Agent', 'CVE', 'Severity', 'Score'],
342
- rows: (data.data || []).map(v => ({
343
- agent: v.agent?.name || '',
344
- cve: v.vulnerability?.id || '',
345
- severity: v.vulnerability?.severity || '',
346
- score: v.vulnerability?.score?.base ?? '',
347
- })),
348
- }, { format: program.opts().output, emptyMessage: 'No vulnerabilities found.' });
349
- });
350
-
351
- wazuh
352
- .command('deploy-history')
353
- .description('Show Wazuh agent deploy history')
354
- .option('--mode <mode>', 'latest|all', 'latest')
355
- .option('--limit <n>', 'max results (mode=all only)', '200')
356
- .action(async (options) => {
357
- auth.requireAuth();
358
- const ep = resolveEndpoint('mon.wazuh.deploy-history.list');
359
- const data = await client.get(ep.path, { service: ep.service, query: { mode: options.mode, limit: options.limit } });
360
- formatOutput({
361
- // fleet #385 class sweep: the route returns
362
- // { hostname, ip, ssh_user, success, message, deployed_at }, but this
363
- // declared hostIp/status/deployedAt — three of four columns rendered
364
- // as dashes with no error. `success` is a boolean, so it's mapped to
365
- // a readable status rather than printed raw.
366
- columns: ['hostname', 'ip', 'status', 'deployed_at'],
367
- headers: ['Host', 'IP', 'Status', 'Deployed'],
368
- rows: (data.deploys || []).map(d => ({
369
- ...d,
370
- status: d.success ? 'ok' : (d.message ? `failed: ${String(d.message).slice(0, 40)}` : 'failed'),
371
- })),
372
- }, { format: program.opts().output, emptyMessage: 'No deploy history found.' });
373
- });
277
+ // Read verbs (status, agents, alerts, vulns, deploy-history) are the top-level `quox wazuh` implementation: one code path.
278
+ // WAZ-10: the three legacy write verbs keep their flags but run through the SAME governed helpers as
279
+ // `quox wazuh deploy|deploy-bulk|active-response` (wazuh_* tools, held for a human approval). The old
280
+ // /api/v1/wazuh write routes are retired (410).
281
+ const { guard, writeTool, deployOne, WazFail } = attachWazuhVerbs(wazuh, program, { client, auth }, { writes: false });
282
+ const padId = (i) => (/^\d+$/.test(i) ? i.padStart(3, '0') : i);
283
+ const refuseTargetOverride = (verb, options) => {
284
+ if (options.wazuhServer || options.bastionHost) {
285
+ throw new WazFail(`quox mon wazuh ${verb}: --wazuh-server and --bastion-host are not accepted on the governed path, which always uses the org's configured Wazuh manager and bastion. Drop them, or change the Wazuh plugin configuration. Governed verb: quox wazuh ${verb === 'deploy-agent' ? 'deploy' : verb}`);
286
+ }
287
+ };
374
288
 
375
289
  wazuh
376
290
  .command('deploy-agent')
377
- .description('Queue a Wazuh agent deploy for one host (HITL-gated — requires HITL Inbox approval)')
291
+ .description('Install the Wazuh agent on one host (governed wazuh_deploy_agent, held for human approval; same as `quox wazuh deploy`)')
378
292
  .requiredOption('--hostname <hostname>', 'target hostname')
379
293
  .requiredOption('--host-ip <ip>', 'target host IP')
380
- .option('--wazuh-server <ip>', 'Wazuh manager IP (defaults to server config)')
381
- .option('--bastion-host <ip>', 'bastion host for the SSH hop')
294
+ .option('--wazuh-server <ip>', 'not accepted: the governed path uses the org\'s configured Wazuh manager')
295
+ .option('--bastion-host <ip>', 'not accepted: the governed path uses the org\'s configured bastion')
382
296
  .option('--ssh-user <user>', 'SSH user', 'control')
383
297
  .option('--ssh-port <n>', 'SSH port', '22')
384
298
  .option('--force', 'skip confirmation')
385
- .action(async (options) => {
299
+ .action(guard(async (options) => {
386
300
  auth.requireAuth();
387
- const confirmed = await confirmOrSkip(program, options, `Queue Wazuh agent deploy to ${options.hostname} (${options.hostIp})? This will require HITL Inbox approval before it runs.`);
301
+ refuseTargetOverride('deploy-agent', options);
302
+ const confirmed = await confirmOrSkip(program, options, `Request a Wazuh agent deploy to ${options.hostname} (${options.hostIp})? It is held for human approval before it runs.`);
388
303
  if (!confirmed) {
389
304
  printInfo('Cancelled.');
390
305
  return;
391
306
  }
392
- const body = { hostname: options.hostname, hostIp: options.hostIp, sshUser: options.sshUser, sshPort: options.sshPort };
393
- if (options.wazuhServer) body.wazuhServer = options.wazuhServer;
394
- if (options.bastionHost) body.bastionHost = options.bastionHost;
395
- const ep = resolveEndpoint('mon.wazuh.deploy-agent');
396
- const data = await client.post(ep.path, { service: ep.service, body });
397
- printSuccess(`Deploy queued — inbox item ${data.inbox_item_id} (awaiting approval).`);
398
- });
307
+ await deployOne(options.hostname, options.hostIp, {
308
+ sshUser: options.sshUser && options.sshUser !== 'control' ? options.sshUser : undefined,
309
+ sshPort: options.sshPort && String(options.sshPort) !== '22' ? options.sshPort : undefined,
310
+ });
311
+ }));
399
312
 
400
313
  wazuh
401
314
  .command('deploy-bulk')
402
- .description('Queue a Wazuh agent deploy for multiple hosts (HITL-gated — requires HITL Inbox approval)')
403
- .requiredOption('--hosts <json>', 'JSON array of {hostname, ip} objects')
404
- .option('--wazuh-server <ip>', 'Wazuh manager IP (defaults to server config)')
405
- .option('--bastion-host <ip>', 'bastion host for the SSH hop')
315
+ .description('Install the Wazuh agent on several hosts: one governed wazuh_deploy_agent call and one approval per host (same as `quox wazuh deploy-bulk`)')
316
+ .requiredOption('--hosts <json>', 'JSON array of {hostname, ip[, sshUser, sshPort]} objects')
317
+ .option('--wazuh-server <ip>', 'not accepted: the governed path uses the org\'s configured Wazuh manager')
318
+ .option('--bastion-host <ip>', 'not accepted: the governed path uses the org\'s configured bastion')
406
319
  .option('--force', 'skip confirmation')
407
320
  .action(async (options) => {
408
321
  auth.requireAuth();
@@ -410,40 +323,49 @@ export function registerMonCommands(program, ctx) {
410
323
  if (!Array.isArray(hosts) || hosts.length === 0) {
411
324
  throw new Error('--hosts must be a non-empty JSON array of {hostname, ip} objects.');
412
325
  }
413
- const confirmed = await confirmOrSkip(program, options, `Queue Wazuh agent deploy to ${hosts.length} host(s)? This will require HITL Inbox approval before it runs.`);
414
- if (!confirmed) {
415
- printInfo('Cancelled.');
416
- return;
417
- }
418
- const body = { hosts };
419
- if (options.wazuhServer) body.wazuhServer = options.wazuhServer;
420
- if (options.bastionHost) body.bastionHost = options.bastionHost;
421
- const ep = resolveEndpoint('mon.wazuh.deploy-bulk');
422
- const data = await client.post(ep.path, { service: ep.service, body });
423
- printSuccess(`Bulk deploy queued for ${data.host_count ?? hosts.length} host(s) — inbox item ${data.inbox_item_id} (awaiting approval).`);
326
+ await guard(async () => {
327
+ refuseTargetOverride('deploy-bulk', options);
328
+ const missing = hosts.filter((h) => !h || !h.hostname || !h.ip).map((h) => (h && h.hostname) || '?');
329
+ if (missing.length) throw new WazFail(`quox mon wazuh deploy-bulk: every host needs hostname and ip; missing for ${missing.join(', ')}`);
330
+ const confirmed = await confirmOrSkip(program, options, `Request a Wazuh agent deploy to ${hosts.length} host(s)? Each is held for human approval before it runs.`);
331
+ if (!confirmed) {
332
+ printInfo('Cancelled.');
333
+ return;
334
+ }
335
+ let failed = 0;
336
+ for (const h of hosts) {
337
+ try { await deployOne(String(h.hostname), String(h.ip), { sshUser: h.sshUser, sshPort: h.sshPort }); } catch (err) {
338
+ if (!(err instanceof WazFail)) throw err;
339
+ console.error(err.message); failed++;
340
+ }
341
+ }
342
+ if (failed) process.exitCode = 1;
343
+ })();
424
344
  });
425
345
 
426
346
  wazuh
427
347
  .command('active-response')
428
- .description('Trigger a Wazuh active response (HITL-gated — requires HITL Inbox approval)')
348
+ .description('Run a Wazuh active response (governed wazuh_active_response, held for human approval; same as `quox wazuh active-response`)')
429
349
  .requiredOption('--command <command>', 'firewall-block|host-isolate|restart-wazuh-agent')
430
350
  .requiredOption('--agents <ids>', 'comma-separated Wazuh agent IDs')
431
351
  .requiredOption('--reason <text>', 'justification (min 10 characters)')
432
352
  .option('--args <args>', 'comma-separated command arguments')
433
353
  .option('--force', 'skip confirmation')
434
- .action(async (options) => {
354
+ .action(guard(async (options) => {
435
355
  auth.requireAuth();
436
- const agentList = options.agents.split(',').map(a => a.trim()).filter(Boolean);
356
+ const agentList = options.agents.split(',').map(a => a.trim()).filter(Boolean).map(padId);
437
357
  const args = options.args ? options.args.split(',').map(a => a.trim()).filter(Boolean) : [];
438
- const confirmed = await confirmOrSkip(program, options, `Trigger active response "${options.command}" on ${agentList.length} agent(s)? This will require HITL Inbox approval before it executes.`);
358
+ if (!agentList.length) throw new WazFail('quox mon wazuh active-response: --agents needs at least one agent id');
359
+ if (!options.reason || options.reason.trim().length < 10) throw new WazFail('quox mon wazuh active-response: --reason "<why>" is required, at least 10 characters (the approver sees it)');
360
+ const confirmed = await confirmOrSkip(program, options, `Request active response "${options.command}" on ${agentList.length} agent(s)? It is held for human approval before it runs.`);
439
361
  if (!confirmed) {
440
362
  printInfo('Cancelled.');
441
363
  return;
442
364
  }
443
- const ep = resolveEndpoint('mon.wazuh.active-response');
444
- const data = await client.post(ep.path, { service: ep.service, body: { command: options.command, agentList, args, reason: options.reason } });
445
- printSuccess(`Active response queued — inbox item ${data.inbox_item_id} (awaiting approval).`);
446
- });
365
+ const input = { command: options.command, agent_ids: agentList, reason: options.reason.trim() };
366
+ if (args.length) input.args = args;
367
+ await writeTool(`active-response ${agentList.join(',')}`, 'wazuh_active_response', input, `${options.command} ran on ${agentList.length} agent(s)`);
368
+ }));
447
369
 
448
370
  // ══════════════════════════════════════════════════════════════════════
449
371
  // agh-chain
@@ -461,6 +383,7 @@ export function registerMonCommands(program, ctx) {
461
383
  auth.requireAuth();
462
384
  const ep = resolveEndpoint('mon.agh-chain.leaderboard.list');
463
385
  const data = await client.get(ep.path, { service: ep.service, query: { window: options.window } });
386
+ if (reportComputing(program, data, 'The leaderboard is computing, try again shortly.')) return;
464
387
  formatOutput({
465
388
  columns: ['agent_id', 'claims_made', 'claims_verified', 'claims_stripped', 'unfulfilled_promises', 'honesty_rate'],
466
389
  headers: ['Agent', 'Claims Made', 'Verified', 'Stripped', 'Unfulfilled', 'Honesty Rate'],
@@ -477,6 +400,7 @@ export function registerMonCommands(program, ctx) {
477
400
  auth.requireAuth();
478
401
  const ep = resolveEndpoint('mon.agh-chain.leaderboard.get', { agentId });
479
402
  const data = await client.get(ep.path, { service: ep.service, query: { window: options.window } });
403
+ if (reportComputing(program, data, 'These honesty metrics are computing, try again shortly.')) return;
480
404
  formatDetail(data, { format: program.opts().output, title: `Honesty metrics: ${agentId}` });
481
405
  });
482
406
 
@@ -490,6 +414,7 @@ export function registerMonCommands(program, ctx) {
490
414
  auth.requireAuth();
491
415
  const ep = resolveEndpoint('mon.agh-chain.leaderboard.events.list', { agentId });
492
416
  const data = await client.get(ep.path, { service: ep.service, query: { window: options.window, guard: options.guard } });
417
+ if (reportComputing(program, data, 'These drill-through events are computing, try again shortly.')) return;
493
418
  formatOutput({
494
419
  columns: ['event_id', 'run_id', 'ts', 'payload_preview'],
495
420
  headers: ['Event', 'Run', 'Time', 'Preview'],
@@ -10,10 +10,46 @@
10
10
  * GET /api/v1/health/aggregate — same endpoint `quox status` uses
11
11
  * GET /governance/incidents — list
12
12
  * GET /governance/incidents/:id — detail + linked AEE envelopes
13
+ * POST /governance/incidents/:id/transition — claim (self only) / move status, witnessed
14
+ * (quox.governance.incident.updated; lib/governanceIncidentLifecycle.js)
15
+ *
16
+ * `incidents list --json` prints a bare array of incidents, the CLI-wide
17
+ * formatOutput convention for list verbs; proof cases read it by index
18
+ * (docs/matrix/suites/monitoring.json mon-incidents-open, mon-incident-detail),
19
+ * so the shape stays as is. `get --json` prints the incident object.
13
20
  */
14
21
 
15
22
  import { formatOutput, formatDetail } from '../core/output.js';
16
23
  import { resolveEndpoint } from '../api/endpoints.js';
24
+ import { QuoxError } from '../core/errors.js';
25
+
26
+ // WAZ P11 slice 1b: statuses a person can move an incident to from the CLI. The collector
27
+ // validates against INCIDENT_STATUSES and enforces the resolution rule too; this only saves a call.
28
+ export const MOVE_STATUSES = ['investigating', 'resolved', 'closed', 'false_positive'];
29
+ const NEEDS_RESOLUTION = new Set(['resolved', 'closed', 'false_positive']);
30
+ const RESOLUTION_MAX = 500;
31
+
32
+ function printTransition(verb, id, data) {
33
+ const inc = data.incident || {};
34
+ const t = data.transition || {};
35
+ const w = data.witnessed || {};
36
+ const who = inc.assigned_to ? ` · assigned to ${inc.assigned_to}` : '';
37
+ const move = t.from && t.to ? `${t.from} -> ${t.to}` : (inc.status || '-');
38
+ const wit = w.stored ? `witnessed${w.id ? ` (${w.id})` : ''}` : `NOT witnessed (${w.reason || 'no envelope stored'})`;
39
+ console.log(`${w.stored ? '✔' : '⚠'} incidents ${verb} │ ${inc.id || id} · ${move}${who} │ ${wit}`);
40
+ if (inc.resolution && NEEDS_RESOLUTION.has(inc.status)) console.log(` resolution: ${inc.resolution}`);
41
+ }
42
+
43
+ // Module level (not a closure) so cli/scripts/export-leaf-methods.mjs sees the POST as evidence for both leaves.
44
+ async function postTransition(client, program, verb, id, body) {
45
+ const ep = resolveEndpoint('governance.incidents.transition', { id });
46
+ const data = await client.post(ep.path, { service: ep.service, body });
47
+ if (!data || data.error || !data.incident) {
48
+ throw new QuoxError(`quox monitoring incidents ${verb}: ${ep.method} ${ep.path} returned an error body: ${(data && (data.error || data.message)) || 'response had no incident'}`, 1);
49
+ }
50
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
51
+ printTransition(verb, id, data);
52
+ }
17
53
 
18
54
  export function registerMonitoringCommands(program, ctx) {
19
55
  const { client, auth } = ctx;
@@ -83,4 +119,36 @@ export function registerMonitoringCommands(program, ctx) {
83
119
  const data = await client.get(resolveEndpoint('governance.incidents.get', { id }).path, { service: resolveEndpoint('governance.incidents.get', { id }).service });
84
120
  formatDetail(data.incident || data, { format: program.opts().output, title: `Incident: ${id}` });
85
121
  });
122
+
123
+ incidents
124
+ .command('claim')
125
+ .description('Claim a governance incident: assigns it to you and moves open to investigating (witnessed)')
126
+ .argument('<id>', 'incident ID from `quox monitoring incidents list`')
127
+ .action(async (id) => {
128
+ auth.requireAuth();
129
+ await postTransition(client, program, 'claim', id, { assigned_to: 'me' });
130
+ });
131
+
132
+ incidents
133
+ .command('status')
134
+ .description(`Move a governance incident to ${MOVE_STATUSES.join('|')}; resolved, closed and false_positive need --resolution (witnessed)`)
135
+ .argument('<id>', 'incident ID from `quox monitoring incidents list`')
136
+ .argument('<status>', MOVE_STATUSES.join('|'))
137
+ .option('--resolution <text>', `what was done or why it is closing (required for resolved, closed, false_positive; max ${RESOLUTION_MAX} chars)`)
138
+ .action(async (id, status, options) => {
139
+ if (!MOVE_STATUSES.includes(status)) {
140
+ throw new QuoxError(`quox monitoring incidents status: status must be one of: ${MOVE_STATUSES.join(', ')} (got "${status}").`, 1);
141
+ }
142
+ const resolution = typeof options.resolution === 'string' ? options.resolution.trim() : '';
143
+ if (NEEDS_RESOLUTION.has(status) && !resolution) {
144
+ throw new QuoxError(`quox monitoring incidents status: moving to ${status} needs --resolution "<what was done>".`, 1);
145
+ }
146
+ if (resolution.length > RESOLUTION_MAX) {
147
+ throw new QuoxError(`quox monitoring incidents status: --resolution is at most ${RESOLUTION_MAX} characters (got ${resolution.length}).`, 1);
148
+ }
149
+ auth.requireAuth();
150
+ const body = { status };
151
+ if (resolution) body.resolution = resolution;
152
+ await postTransition(client, program, 'status', id, body);
153
+ });
86
154
  }
@@ -10,6 +10,7 @@ import { spawn } from 'child_process';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { basename, dirname, join, resolve } from 'path';
12
12
  import { Option } from 'commander';
13
+ import { resolveProfileFlagFromArgv } from '../core/config.js';
13
14
  import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
14
15
  import { QuoxApiError, QuoxError } from '../core/errors.js';
15
16
  import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
@@ -18,6 +19,7 @@ import { resolveOrgId } from './helpers.js';
18
19
  import { buildBatchArchive, splitMarkdown, indexMarkdown, MAX_BATCH_NOTES } from './notesBatch.js';
19
20
  import { emailToNote, extractTasks, promoteBody } from './notesBridges.js';
20
21
  import { linkMention } from './notesLinks.js';
22
+ import { registerNotesSeal } from './notesSeal.js';
21
23
 
22
24
  const SCOPES = ['org', 'user'];
23
25
  const ARCHIVED = ['exclude', 'only', 'include'];
@@ -156,6 +158,8 @@ export function registerNotesCommands(program, ctx) {
156
158
  .command('notes')
157
159
  .description('NotesQ: governed markdown notes (list, read, write, history, restore)');
158
160
 
161
+ registerNotesSeal(notes, { program, client, auth, call, getNote, putNote, readContent, isJson, printJson, announce, loadEnqrypt: ctx.loadEnqrypt });
162
+
159
163
  const list = async (options, textQuery) => {
160
164
  auth.requireAuth();
161
165
  checkEnum(options.scope, [...SCOPES, 'all'], '--scope');
@@ -188,10 +192,18 @@ export function registerNotesCommands(program, ctx) {
188
192
  updated: when(n),
189
193
  }));
190
194
  formatOutput({
191
- columns: ['id', 'title', 'scope', 'collection', 'tags', 'updated', ...propKeys.map((_, i) => `prop${i}`)],
192
- headers: ['ID', 'Title', 'Scope', 'Collection', 'Tags', 'Updated', ...propKeys],
195
+ columns: ['title', 'scope', 'collection', 'tags', 'updated', ...propKeys.map((_, i) => `prop${i}`), 'id'],
196
+ headers: ['Title', 'Scope', 'Collection', 'Tags', 'Updated', ...propKeys, 'ID'],
193
197
  rows,
194
- }, { format: program.opts().output });
198
+ }, {
199
+ format: program.opts().output,
200
+ quietColumn: 'id',
201
+ emptyMessage: 'No notes found. Widen the filters (--scope all, drop --tag/--q), or create one with `quox notes new`.',
202
+ });
203
+ const total = data.total ?? rows.length;
204
+ if (program.opts().output !== 'json' && rows.length > 0 && rows.length < total) {
205
+ console.log(`showing ${rows.length} of ${total} · --limit to see more`);
206
+ }
195
207
  announce('notes ls', `${rows.length} of ${data.total ?? rows.length}`);
196
208
  };
197
209
 
@@ -1116,8 +1128,15 @@ export function registerNotesCommands(program, ctx) {
1116
1128
  auth.requireAuth();
1117
1129
  const ep = resolveEndpoint('notesq.notes.proposeSkill', { id });
1118
1130
  const body = { ...(options.name ? { name: options.name } : {}), ...(options.category ? { category: options.category } : {}) };
1131
+ // The route refuses a note under its minimum length with 400 {error:'too_short', min_words}: say so, not a bare 400.
1132
+ const propose = (b, opts = {}) => call(() => client.post(ep.path, { service: ep.service, body: b, ...opts })).catch((e) => {
1133
+ if (e instanceof QuoxApiError && e.status === 400 && e.body && e.body.error === 'too_short') {
1134
+ throw new QuoxError(`Note ${id} is too short to become a skill: it needs at least ${e.body.min_words || 'more'} words of procedure. Add the steps (quox notes edit ${id}) and run again.`, 1);
1135
+ }
1136
+ throw e;
1137
+ });
1119
1138
  if (!options.yes) {
1120
- const r = await call(() => client.post(ep.path, { service: ep.service, body: { ...body, dry_run: true } }));
1139
+ const r = await propose({ ...body, dry_run: true });
1121
1140
  if (isJson(program)) return printJson(r);
1122
1141
  const p = r.payload || {};
1123
1142
  console.log(`name: ${p.name}`);
@@ -1126,7 +1145,7 @@ export function registerNotesCommands(program, ctx) {
1126
1145
  console.log(String(p.bodyMd || '').split('\n').slice(0, 8).join('\n'));
1127
1146
  return console.log('Re-run with --yes to propose it. It lands as an experimental skill, outside the stable library; an automated safety check reviews it and anything flagged goes to the human review queue (quox skills review).');
1128
1147
  }
1129
- const r = await call(() => client.post(ep.path, { service: ep.service, body, retries: 0 }));
1148
+ const r = await propose(body, { retries: 0 });
1130
1149
  if (isJson(program)) return printJson(r);
1131
1150
  announce('propose-skill', `${r.skill.name} (${r.skill.id}) experimental`);
1132
1151
  if (!r.note_updated) console.log('Note: the skill was created but the note could not be updated with the link (it changed meanwhile).');
@@ -1225,7 +1244,7 @@ export function registerNotesCommands(program, ctx) {
1225
1244
 
1226
1245
  notes
1227
1246
  .command('scorecard')
1228
- .description('Run the NotesQ scorecard (scripts/notesq-scorecard.mjs in the dashboard repo), streaming its output')
1247
+ .description('Run the NotesQ scorecard (scripts/notesq-scorecard.mjs in the dashboard repo), streaming its output. WRITE: creates notes through chat (raising approval cards) and deletes them afterwards; the child runs as the same -p profile')
1229
1248
  .option('--keep', 'keep the scorecard\'s test data')
1230
1249
  .action(async (options) => {
1231
1250
  if (!existsSync(scorecardPath)) {
@@ -1235,7 +1254,10 @@ export function registerNotesCommands(program, ctx) {
1235
1254
  const args = [scorecardPath];
1236
1255
  if (options.keep) args.push('--keep');
1237
1256
  const code = await new Promise((done, fail) => {
1238
- const child = spawn(process.execPath, args, { cwd: resolve(dirname(scorecardPath), '..'), stdio: ['inherit', 'pipe', 'pipe'] });
1257
+ // The child must act as the profile the caller chose, never the resident/attached agent identity.
1258
+ const profile = ctx.profile || resolveProfileFlagFromArgv(process.argv.slice(2)) || process.env.QUOX_PROFILE;
1259
+ const env = profile ? { ...process.env, QUOX_PROFILE: profile } : process.env;
1260
+ const child = spawn(process.execPath, args, { cwd: resolve(dirname(scorecardPath), '..'), env, stdio: ['inherit', 'pipe', 'pipe'] });
1239
1261
  child.stdout.on('data', (c) => process.stdout.write(c));
1240
1262
  child.stderr.on('data', (c) => process.stderr.write(c));
1241
1263
  child.on('error', fail);