quoxcli 0.6.0 → 0.6.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/CHANGELOG.md +18 -3
- package/lib/api/endpoints/admin.js +21 -0
- package/lib/api/endpoints/calendarq.js +8 -0
- package/lib/api/endpoints/index.js +2 -0
- package/lib/api/endpoints/lens.js +34 -0
- package/lib/api/endpoints/lucid.js +2 -1
- package/lib/api/endpoints/mon.js +6 -7
- package/lib/api/endpoints/v1.js +57 -0
- package/lib/commands/admin.js +86 -0
- package/lib/commands/approvals.js +5 -1
- package/lib/commands/calendar.js +27 -0
- package/lib/commands/chat.js +1 -1
- package/lib/commands/fleet.js +164 -0
- package/lib/commands/fleetRecipe.js +174 -0
- package/lib/commands/lens.js +268 -0
- package/lib/commands/lucidGoals.js +36 -2
- package/lib/commands/mail.js +21 -2
- package/lib/commands/monitoring.js +5 -1
- package/lib/commands/plan.js +36 -1
- package/lib/commands/research.js +78 -0
- package/lib/commands/settings.js +7 -1
- package/lib/commands/wazuh.js +9 -0
- package/lib/commands/wazuhIntake.js +11 -3
- package/lib/core/commandRegistrars.js +1 -0
- package/lib/core/version.js +15 -2
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/app.js +1 -1
- package/lib/quoxon/commands.js +1 -1
- package/lib/quoxon/dragonCompanion.js +24 -12
- package/lib/quoxon/main.js +1 -1
- package/lib/quoxon/onboarding/view.js +1 -1
- package/lib/quoxon/slash/cliLeaves.json +19 -0
- package/lib/quoxon/whatsnew.jsonl +1 -0
- package/package.json +1 -1
- package/vendor/dragonlab/SOURCE +3 -3
- package/vendor/dragonlab/design/architect/v2/architect-master.svg +67 -0
- package/vendor/dragonlab/design/architect/v2/character.mjs +83 -0
- package/vendor/dragonlab/design/architect/v2/review.mjs +35 -0
- package/vendor/dragonlab/design/architect/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/archivist/v2/archivist-master.svg +77 -0
- package/vendor/dragonlab/design/archivist/v3/character.mjs +56 -0
- package/vendor/dragonlab/design/archivist/v4/character.mjs +46 -0
- package/vendor/dragonlab/design/archivist/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/archivist/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/argus/v2/character.mjs +69 -0
- package/vendor/dragonlab/design/argus/v3/argus-master.svg +68 -0
- package/vendor/dragonlab/design/argus/v3/character.mjs +63 -0
- package/vendor/dragonlab/design/argus/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/argus/v3/watch.mjs +56 -0
- package/vendor/dragonlab/design/daedalus/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/daedalus/v3/daedalus-master.svg +65 -0
- package/vendor/dragonlab/design/daedalus/v4/character.mjs +47 -0
- package/vendor/dragonlab/design/daedalus/v5/character.mjs +66 -0
- package/vendor/dragonlab/design/daedalus/v6/character.mjs +32 -0
- package/vendor/dragonlab/design/daedalus/v6/review.mjs +35 -0
- package/vendor/dragonlab/design/daedalus/v6/watch.mjs +56 -0
- package/vendor/dragonlab/design/janus/v1/janus-master.svg +70 -0
- package/vendor/dragonlab/design/janus/v2/character.mjs +56 -0
- package/vendor/dragonlab/design/janus/v3/character.mjs +7 -0
- package/vendor/dragonlab/design/janus/v4/character.mjs +150 -0
- package/vendor/dragonlab/design/janus/v5/character.mjs +55 -0
- package/vendor/dragonlab/design/janus/v5/review.mjs +35 -0
- package/vendor/dragonlab/design/janus/v5/watch.mjs +56 -0
- package/vendor/dragonlab/design/roady/v1/roady-master.svg +67 -0
- package/vendor/dragonlab/design/roady/v2/character.mjs +69 -0
- package/vendor/dragonlab/design/roady/v3/character.mjs +91 -0
- package/vendor/dragonlab/design/roady/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/roady/v3/watch.mjs +56 -0
- package/vendor/dragonlab/design/weaver/v3/weaver-master.svg +57 -0
- package/vendor/dragonlab/design/weaver/v4/character.mjs +56 -0
- package/vendor/dragonlab/design/weaver/v5/character.mjs +52 -0
- package/vendor/dragonlab/design/weaver/v6/character.mjs +88 -0
- package/vendor/dragonlab/design/weaver/v6/review.mjs +35 -0
- package/vendor/dragonlab/design/weaver/v6/watch.mjs +56 -0
- package/vendor/dragonlab/integration/companions-v1/architect.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/archivist.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/argus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +86 -0
- package/vendor/dragonlab/integration/companions-v1/daedalus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/janus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/roady.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +4 -1
- package/vendor/dragonlab/integration/companions-v1/weaver.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,26 @@ User-visible changes to the `quoxcli` package. Items marked *(unproven)* ship in
|
|
|
4
4
|
the package and pass their tests, but nobody has run them end to end from the
|
|
5
5
|
published package yet.
|
|
6
6
|
|
|
7
|
+
## 0.6.1 (2026-11-10)
|
|
8
|
+
|
|
9
|
+
- `quox --version` and `quox doctor` no longer report another project's git commit. Installed
|
|
10
|
+
under nvm, 0.6.0 printed `0.6.0 (bab86d5, detached)`: `bab86d5` was nvm's own commit,
|
|
11
|
+
because `~/.nvm` is a git clone. Git details now show only when the CLI runs from its own
|
|
12
|
+
source checkout; a package install prints the bare version. *(driven live: the 0.6.0
|
|
13
|
+
npm install patched with this fix printed `0.6.0`)*
|
|
14
|
+
- New: `quox lens panel` runs several QuoxLens critiques over one target, repeated, and keeps the
|
|
15
|
+
findings that recur across runs. *(unproven from the package; landed by another session the
|
|
16
|
+
same day)*
|
|
17
|
+
- `quox lens apply` now waits past the 5 minute model ceiling instead of the 30 second client
|
|
18
|
+
default. *(unproven from the package)*
|
|
19
|
+
|
|
7
20
|
## 0.6.0 (2026-11-10)
|
|
8
21
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
Driven from the published 0.6.0 package on 2026-11-10 (npm install -g, Node under nvm):
|
|
23
|
+
`notes ls`, `doctor` (9 of 9), and the full seal round trip (`notes seal init`, `status`,
|
|
24
|
+
`new`, `open` by id, `print` then `open --armor-file`; the server copy held no plaintext).
|
|
25
|
+
Everything else below has not been run from the package yet. Items marked *(driven live)*
|
|
26
|
+
were driven against a live instance from the source tree before release.
|
|
12
27
|
|
|
13
28
|
### New commands
|
|
14
29
|
- `quox notes seal convert <id>` and `quox notes seal purge <id>` (human sessions only):
|
|
@@ -175,4 +175,25 @@ export const admin = {
|
|
|
175
175
|
path: () => `/api/admin/stats`,
|
|
176
176
|
tags: ['live-safe'],
|
|
177
177
|
},
|
|
178
|
+
// AQ-9H LADON-P1B: the scratch-org registry (fixture mail/calendar providers load only for a marked org, and
|
|
179
|
+
// only on a QUOX_FIXTURE_PROVIDERS=1 deployment). Collector routes, human instance admin only, enforced
|
|
180
|
+
// server-side by requireInstanceAdminMw (services/collector/server.js).
|
|
181
|
+
'admin.scratch_orgs.list': {
|
|
182
|
+
method: 'GET',
|
|
183
|
+
service: 'collector',
|
|
184
|
+
path: () => `/api/v1/admin/scratch-orgs`,
|
|
185
|
+
tags: ['live-safe'],
|
|
186
|
+
},
|
|
187
|
+
'admin.scratch_orgs.mark': {
|
|
188
|
+
method: 'PUT',
|
|
189
|
+
service: 'collector',
|
|
190
|
+
path: ({ orgId }) => `/api/v1/admin/scratch-orgs/${encodeURIComponent(orgId)}`,
|
|
191
|
+
tags: ['admin-write'],
|
|
192
|
+
},
|
|
193
|
+
'admin.scratch_orgs.unmark': {
|
|
194
|
+
method: 'DELETE',
|
|
195
|
+
service: 'collector',
|
|
196
|
+
path: ({ orgId }) => `/api/v1/admin/scratch-orgs/${encodeURIComponent(orgId)}`,
|
|
197
|
+
tags: ['admin-write'],
|
|
198
|
+
},
|
|
178
199
|
};
|
|
@@ -38,6 +38,14 @@ export const calendarq = {
|
|
|
38
38
|
path: () => `/api/v1/calendarq/changes`,
|
|
39
39
|
tags: ['live-safe'],
|
|
40
40
|
},
|
|
41
|
+
// AQ-9H LADON-P1B: create a seeded FIXTURE calendar connection (scratch orgs on a fixture-enabled
|
|
42
|
+
// deployment only; the collector's gate decides). Google connects through the OAuth flow, not here.
|
|
43
|
+
'calendarq.connections.create': {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
service: 'collector',
|
|
46
|
+
path: () => `/api/v1/calendarq/connections`,
|
|
47
|
+
tags: ['live-write'],
|
|
48
|
+
},
|
|
41
49
|
'calendarq.sync': {
|
|
42
50
|
method: 'POST',
|
|
43
51
|
service: 'collector',
|
|
@@ -62,6 +62,7 @@ import { awareness } from './awareness.js';
|
|
|
62
62
|
import { router } from './router.js';
|
|
63
63
|
import { mind } from './mind.js';
|
|
64
64
|
import { lucid } from './lucid.js';
|
|
65
|
+
import { lens } from './lens.js';
|
|
65
66
|
import { loops } from './loops.js';
|
|
66
67
|
import { away } from './away.js';
|
|
67
68
|
import { presence } from './presence.js';
|
|
@@ -156,6 +157,7 @@ export const ENDPOINTS = {
|
|
|
156
157
|
...router,
|
|
157
158
|
...mind,
|
|
158
159
|
...lucid,
|
|
160
|
+
...lens,
|
|
159
161
|
...loops,
|
|
160
162
|
...away,
|
|
161
163
|
...presence,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 'lens.*' endpoints: QuoxLens (`quox lens`).
|
|
3
|
+
* Collector routes under /api/v1/lens (services/collector/routes/quoxlens.js), mounted behind
|
|
4
|
+
* requireServiceOrUserAuth + requirePluginEntitlement('quoxlens') (402 LICENSE_REQUIRED).
|
|
5
|
+
*
|
|
6
|
+
* See cli/lib/api/endpoints/index.js for the fragment convention.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const lens = {
|
|
10
|
+
'lens.list': {
|
|
11
|
+
method: 'GET',
|
|
12
|
+
service: 'collector',
|
|
13
|
+
path: () => `/api/v1/lens/lenses`,
|
|
14
|
+
tags: ['live-safe'],
|
|
15
|
+
},
|
|
16
|
+
'lens.get': {
|
|
17
|
+
method: 'GET',
|
|
18
|
+
service: 'collector',
|
|
19
|
+
path: ({ id }) => `/api/v1/lens/lenses/${encodeURIComponent(id)}`,
|
|
20
|
+
tags: ['live-safe'],
|
|
21
|
+
},
|
|
22
|
+
'lens.apply': {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
service: 'collector',
|
|
25
|
+
path: () => `/api/v1/lens/apply`,
|
|
26
|
+
tags: ['live-write'],
|
|
27
|
+
},
|
|
28
|
+
'lens.panel': {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
service: 'collector',
|
|
31
|
+
path: () => `/api/v1/lens/panel`,
|
|
32
|
+
tags: ['live-write'],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -97,9 +97,10 @@ export const lucid = {
|
|
|
97
97
|
'lucid.world.archive': { method: 'POST', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}/archive`, tags: ['live-write'] },
|
|
98
98
|
'lucid.world.restore': { method: 'POST', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}/restore`, tags: ['live-write'] },
|
|
99
99
|
'lucid.world.history': { method: 'GET', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}/history`, tags: ['live-safe'] },
|
|
100
|
-
// LUCID-INTELLIGENCE LI-2 shadow insights (read-only).
|
|
100
|
+
// LUCID-INTELLIGENCE LI-2 shadow insights (read-only, except a person deciding a proposed link).
|
|
101
101
|
'lucid.insights.list': { method: 'GET', service: 'collector', path: () => `/api/v1/lucid/intelligence/insights`, tags: ['live-safe'] },
|
|
102
102
|
'lucid.insights.status': { method: 'GET', service: 'collector', path: () => `/api/v1/lucid/intelligence/insights/status`, tags: ['live-safe'] },
|
|
103
|
+
'lucid.insights.decide': { method: 'POST', service: 'collector', path: ({ id, goalId }) => `/api/v1/lucid/intelligence/insights/${encodeURIComponent(id)}/links/${encodeURIComponent(goalId)}`, tags: ['live-write'] },
|
|
103
104
|
'lucid.insights.get': { method: 'GET', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/insights/${encodeURIComponent(id)}`, tags: ['live-safe'] },
|
|
104
105
|
// LUCID-INTELLIGENCE LI-3 brief (read-only, never moves a cursor).
|
|
105
106
|
'lucid.brief': { method: 'GET', service: 'collector', path: () => `/api/v1/lucid/intelligence/brief`, tags: ['live-safe'] },
|
package/lib/api/endpoints/mon.js
CHANGED
|
@@ -6,13 +6,12 @@
|
|
|
6
6
|
* collector-direct (no org segment in the URL — same shape as
|
|
7
7
|
* `traces.*`/`aee.*`):
|
|
8
8
|
*
|
|
9
|
-
* - Docker fleet (`/docker/*`) —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* backend itself would serve these to an unauthenticated caller.
|
|
9
|
+
* - Docker fleet (`/docker/*`) — the instance operator's fleet. Every
|
|
10
|
+
* route needs a login (requireServiceOrUserAuth). The reads run
|
|
11
|
+
* instanceFleetRouteGate: the operator org or an instance admin sees the
|
|
12
|
+
* fleet, any other org gets an honest empty list (or 404 for one
|
|
13
|
+
* container). The start/stop/restart action goes through executeTool and
|
|
14
|
+
* is refused for any other org (VULCAN FND_01M71D6KXP402M7CE1NGH0GVVZ).
|
|
16
15
|
* - Wazuh (`/api/v1/wazuh/*`) — gated by `requireServiceOrUserAuth()`.
|
|
17
16
|
* Reads (manager/status, agents, alerts, vulnerabilities,
|
|
18
17
|
* deploy-history) are not org-filtered by the Wazuh API itself (the org
|
package/lib/api/endpoints/v1.js
CHANGED
|
@@ -67,6 +67,38 @@ export const v1 = {
|
|
|
67
67
|
path: () => `/api/v1/fleet/ping`,
|
|
68
68
|
tags: ['admin-write'],
|
|
69
69
|
},
|
|
70
|
+
// BASTION-JANUS P3 — `quox fleet recipe list|show|add|remove|run`: the org's governed fleet
|
|
71
|
+
// recipe library. run hands the rendered recipe to the collector's ssh_exec gate (one card).
|
|
72
|
+
'v1.fleet.recipes.list': {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
service: 'collector',
|
|
75
|
+
path: () => `/api/v1/fleet/recipes`,
|
|
76
|
+
tags: ['live-safe'],
|
|
77
|
+
},
|
|
78
|
+
'v1.fleet.recipes.get': {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
service: 'collector',
|
|
81
|
+
path: ({ name }) => `/api/v1/fleet/recipes/${encodeURIComponent(name)}`,
|
|
82
|
+
tags: ['live-safe'],
|
|
83
|
+
},
|
|
84
|
+
'v1.fleet.recipes.create': {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
service: 'collector',
|
|
87
|
+
path: () => `/api/v1/fleet/recipes`,
|
|
88
|
+
tags: ['admin-write'],
|
|
89
|
+
},
|
|
90
|
+
'v1.fleet.recipes.delete': {
|
|
91
|
+
method: 'DELETE',
|
|
92
|
+
service: 'collector',
|
|
93
|
+
path: ({ name }) => `/api/v1/fleet/recipes/${encodeURIComponent(name)}`,
|
|
94
|
+
tags: ['admin-write'],
|
|
95
|
+
},
|
|
96
|
+
'v1.fleet.recipes.run': {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
service: 'collector',
|
|
99
|
+
path: () => `/api/v1/fleet/recipes/run`,
|
|
100
|
+
tags: ['live-write'],
|
|
101
|
+
},
|
|
70
102
|
// DEVSUITE-HUB Phase 2 — `quox dev enable` (see lib/commands/dev.js).
|
|
71
103
|
// Constructs + signs + delivers a quoxagent dev.CapabilityProfile onto a
|
|
72
104
|
// target host via Bastion. See services/collector/server.js's
|
|
@@ -202,6 +234,31 @@ export const v1 = {
|
|
|
202
234
|
path: () => `/api/v1/build-status`,
|
|
203
235
|
tags: ['live-safe'],
|
|
204
236
|
},
|
|
237
|
+
// HYPERION-NEXT P10: fleet hardware health (bare-metal IPMI / RAID / SMART), `quox fleet hw`.
|
|
238
|
+
'v1.fleet.hw.list': {
|
|
239
|
+
method: 'GET',
|
|
240
|
+
service: 'collector',
|
|
241
|
+
path: () => `/api/v1/fleet/hw`,
|
|
242
|
+
tags: ['live-safe'],
|
|
243
|
+
},
|
|
244
|
+
'v1.fleet.hw.events': {
|
|
245
|
+
method: 'GET',
|
|
246
|
+
service: 'collector',
|
|
247
|
+
path: () => `/api/v1/fleet/hw/events`,
|
|
248
|
+
tags: ['live-safe'],
|
|
249
|
+
},
|
|
250
|
+
'v1.fleet.hw.get': {
|
|
251
|
+
method: 'GET',
|
|
252
|
+
service: 'collector',
|
|
253
|
+
path: ({ host }) => `/api/v1/fleet/hw/${encodeURIComponent(host)}`,
|
|
254
|
+
tags: ['live-safe'],
|
|
255
|
+
},
|
|
256
|
+
'v1.fleet.hw.refresh': {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
service: 'collector',
|
|
259
|
+
path: () => `/api/v1/fleet/hw/refresh`,
|
|
260
|
+
tags: ['live-write'],
|
|
261
|
+
},
|
|
205
262
|
'v1.gpu.nodes.list': {
|
|
206
263
|
method: 'GET',
|
|
207
264
|
service: 'collector',
|
package/lib/commands/admin.js
CHANGED
|
@@ -407,4 +407,90 @@ export function registerAdminCommands(program, ctx) {
|
|
|
407
407
|
const data = await client.get(resolveEndpoint('admin.retention.stats.list').path, { service: resolveEndpoint('admin.retention.stats.list').service });
|
|
408
408
|
formatDetail(data.stats || data, { format: program.opts().output, title: 'Retention Stats' });
|
|
409
409
|
});
|
|
410
|
+
|
|
411
|
+
// AQ-9H LADON-P1B (plan v6.2 A9, owner decision S9): the scratch-org registry. Fixture mail and calendar
|
|
412
|
+
// providers load only for an org marked here, and only on a deployment that sets QUOX_FIXTURE_PROVIDERS=1.
|
|
413
|
+
// The collector refuses every caller but a human instance admin (requireInstanceAdminMw), so this verb is a
|
|
414
|
+
// human step: an agent or service key gets a 403.
|
|
415
|
+
const scratchOrgs = admin
|
|
416
|
+
.command('scratch-orgs')
|
|
417
|
+
.description('Scratch orgs: only a human instance admin can mark an org, which enables fixture mail and calendar providers for that org only');
|
|
418
|
+
|
|
419
|
+
scratchOrgs
|
|
420
|
+
.command('list')
|
|
421
|
+
.description('List the orgs marked scratch (human instance admin)')
|
|
422
|
+
.action(async () => {
|
|
423
|
+
auth.requireAuth();
|
|
424
|
+
const ep = resolveEndpoint('admin.scratch_orgs.list');
|
|
425
|
+
let data;
|
|
426
|
+
try {
|
|
427
|
+
data = await client.get(ep.path, { service: ep.service });
|
|
428
|
+
} catch (err) {
|
|
429
|
+
announceFailure('quox', 'admin scratch-orgs list', err);
|
|
430
|
+
throw err;
|
|
431
|
+
}
|
|
432
|
+
const orgs = data.orgs || [];
|
|
433
|
+
announce('quox', 'admin scratch-orgs list', data.corrupt
|
|
434
|
+
? { result: 'registry unreadable', status: 'warn' }
|
|
435
|
+
: { result: `${orgs.length} scratch org(s)` });
|
|
436
|
+
if (program.opts().output === 'json') {
|
|
437
|
+
console.log(JSON.stringify(data, null, 2));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (data.corrupt) {
|
|
441
|
+
console.error(`Scratch-org registry is unreadable, so NO org is treated as scratch: ${data.error || 'unknown error'}`);
|
|
442
|
+
process.exitCode = 1;
|
|
443
|
+
}
|
|
444
|
+
formatOutput({
|
|
445
|
+
columns: ['orgId', 'markedBy', 'markedAt', 'note'],
|
|
446
|
+
headers: ['Org', 'Marked by', 'Marked at', 'Note'],
|
|
447
|
+
rows: orgs.map((o) => ({ orgId: o.orgId, markedBy: o.markedBy || '-', markedAt: o.markedAt || '-', note: o.note || '' })),
|
|
448
|
+
}, { format: program.opts().output, raw: orgs, emptyMessage: 'No org is marked scratch.' });
|
|
449
|
+
if (program.opts().output !== 'quiet' && program.opts().output !== 'csv') {
|
|
450
|
+
console.log(data.fixtureProvidersEnv
|
|
451
|
+
? 'Fixture providers are enabled on this deployment (QUOX_FIXTURE_PROVIDERS=1).'
|
|
452
|
+
: 'Fixture providers are off on this deployment (QUOX_FIXTURE_PROVIDERS is not 1), so marking has no effect yet.');
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
scratchOrgs
|
|
457
|
+
.command('mark')
|
|
458
|
+
.description('Mark an org scratch so fixture providers load for that org only (human instance admin; audited)')
|
|
459
|
+
.argument('<orgId>', 'the org id to mark')
|
|
460
|
+
.option('--note <text>', 'why this org is scratch (stored with the mark, 200 characters max)')
|
|
461
|
+
.action(async (orgId, opts) => {
|
|
462
|
+
auth.requireAuth();
|
|
463
|
+
const ep = resolveEndpoint('admin.scratch_orgs.mark', { orgId });
|
|
464
|
+
let data;
|
|
465
|
+
try {
|
|
466
|
+
data = await client.put(ep.path, { service: ep.service, body: opts.note ? { note: opts.note } : {} });
|
|
467
|
+
} catch (err) {
|
|
468
|
+
announceFailure('quox', `admin scratch-orgs mark ${orgId}`, err);
|
|
469
|
+
throw err;
|
|
470
|
+
}
|
|
471
|
+
announce('quox', `admin scratch-orgs mark ${orgId}`, { result: 'marked scratch' });
|
|
472
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
473
|
+
printSuccess(`Org ${orgId} is marked scratch. Fixture providers load for this org only, on a deployment with QUOX_FIXTURE_PROVIDERS=1.`);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
scratchOrgs
|
|
477
|
+
.command('unmark')
|
|
478
|
+
.description('Remove the scratch mark from an org, so fixture providers no longer load for it (human instance admin; audited)')
|
|
479
|
+
.argument('<orgId>', 'the org id to unmark')
|
|
480
|
+
.action(async (orgId) => {
|
|
481
|
+
auth.requireAuth();
|
|
482
|
+
const ep = resolveEndpoint('admin.scratch_orgs.unmark', { orgId });
|
|
483
|
+
let data;
|
|
484
|
+
try {
|
|
485
|
+
data = await client.delete(ep.path, { service: ep.service });
|
|
486
|
+
} catch (err) {
|
|
487
|
+
announceFailure('quox', `admin scratch-orgs unmark ${orgId}`, err);
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
490
|
+
announce('quox', `admin scratch-orgs unmark ${orgId}`, { result: data.removed ? 'unmarked' : 'was not marked' });
|
|
491
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
492
|
+
printSuccess(data.removed
|
|
493
|
+
? `Org ${orgId} is no longer scratch. Fixture providers no longer load for it.`
|
|
494
|
+
: `Org ${orgId} was not marked scratch. Nothing changed.`);
|
|
495
|
+
});
|
|
410
496
|
}
|
|
@@ -142,7 +142,11 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
142
142
|
body.patterns = options.pattern || [];
|
|
143
143
|
}
|
|
144
144
|
const ep = resolveEndpoint('v1.approvals.grants.create');
|
|
145
|
-
|
|
145
|
+
// VULCAN: a standing grant pre-authorises ssh_exec for days, so the server asks for a step-up PIN (scope override).
|
|
146
|
+
const data = await withStepUp(
|
|
147
|
+
(headers) => client.post(ep.path, { service: ep.service, headers, body }),
|
|
148
|
+
{ client, orgId: auth.getOrgId?.(program.opts().org) || null, scope: 'override', io: ctx.stepUpIO, what: 'Creating a standing grant' },
|
|
149
|
+
);
|
|
146
150
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
147
151
|
const g = data.grant || {};
|
|
148
152
|
announce('approvals', `grant created ${g.tool || 'ssh_exec'}@${g.host || options.host}`, { result: g.grant_id });
|
package/lib/commands/calendar.js
CHANGED
|
@@ -28,6 +28,33 @@ export function registerCalendarCommands(program, ctx) {
|
|
|
28
28
|
.command('calendar')
|
|
29
29
|
.description('Calendar-Q: connected calendars, agenda, coverage, sync');
|
|
30
30
|
|
|
31
|
+
// ── connections add (AQ-9H LADON-P1B: fixture only) ─────────────────────────────
|
|
32
|
+
|
|
33
|
+
const connections = calendar
|
|
34
|
+
.command('connections')
|
|
35
|
+
.description('Calendar connections (Google connects through the OAuth flow; add here is fixture only)');
|
|
36
|
+
|
|
37
|
+
connections
|
|
38
|
+
.command('add')
|
|
39
|
+
.description('Connect a seeded fixture calendar for scratch-org proof runs (--provider fixture --seed <name>). The server decides: it refuses unless this deployment enables fixture providers and a platform admin marked the org scratch. Google calendars connect through the OAuth flow, not this command.')
|
|
40
|
+
.requiredOption('--provider <name>', 'calendar provider; only fixture is accepted here')
|
|
41
|
+
.option('--seed <name>', 'fixture seed to load (e.g. scratch-office-day)')
|
|
42
|
+
.option('--run <id>', 'proof run id substituted into the seed ({{run}})')
|
|
43
|
+
.action(async (options) => {
|
|
44
|
+
auth.requireAuth();
|
|
45
|
+
if (options.provider !== 'fixture') {
|
|
46
|
+
throw new Error(`--provider ${options.provider} is not connected here. Only --provider fixture is; Google calendars connect through the OAuth flow.`);
|
|
47
|
+
}
|
|
48
|
+
if (!options.seed) throw new Error('--seed <name> is required with --provider fixture (e.g. --seed scratch-office-day).');
|
|
49
|
+
const body = { provider: 'fixture', seed: options.seed };
|
|
50
|
+
if (options.run) body.run = options.run;
|
|
51
|
+
const ep = resolveEndpoint('calendarq.connections.create');
|
|
52
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
53
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
54
|
+
const conn = data.connection || data;
|
|
55
|
+
printSuccess(`Connected fixture calendar ${conn.resourceLabel || ''} (${conn.id}) from seed ${options.seed}.`);
|
|
56
|
+
});
|
|
57
|
+
|
|
31
58
|
// ── status ───────────────────────────────────────────────────────────
|
|
32
59
|
|
|
33
60
|
calendar
|
package/lib/commands/chat.js
CHANGED
|
@@ -221,7 +221,7 @@ async function awaitTurn(client, turnId) {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/** One-shot (non-streaming) chat via POST /chat/claude, kept server side as a pollable turn. */
|
|
224
|
-
async function sendOneShot(client, { message, agent, sessionId, resumeTurn }) {
|
|
224
|
+
export async function sendOneShot(client, { message, agent, sessionId, resumeTurn }) {
|
|
225
225
|
let data;
|
|
226
226
|
if (resumeTurn) {
|
|
227
227
|
data = await awaitTurn(client, resumeTurn);
|
package/lib/commands/fleet.js
CHANGED
|
@@ -10,6 +10,51 @@ import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } fro
|
|
|
10
10
|
import { events, printEvent, line as glyphLine } from '../core/quoxglyph.js';
|
|
11
11
|
import { parseJSON } from './helpers.js';
|
|
12
12
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
13
|
+
import { registerFleetRecipeCommands } from './fleetRecipe.js';
|
|
14
|
+
|
|
15
|
+
/** HYPERION-NEXT P10: one `quox fleet hw` summary row. */
|
|
16
|
+
function formatHwRow(h) {
|
|
17
|
+
const c = h.components || {};
|
|
18
|
+
return {
|
|
19
|
+
host: h.host,
|
|
20
|
+
overall: h.overall,
|
|
21
|
+
fans: c.fans || '-', psu: c.psu || '-', memory: c.memory || '-', cpu: c.cpu || '-',
|
|
22
|
+
raid: c.raid || '-', disks: c.disks || '-', temps: c.temps || '-',
|
|
23
|
+
findings: h.findings ?? 0,
|
|
24
|
+
checked: h.checked_at ? h.checked_at.replace('T', ' ').slice(0, 16) : '-',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* When a finding was seen. Event findings carry the earliest and latest REAL dated event (an undated "Pre-Init"
|
|
30
|
+
* SEL entry has none); a run over several days shows both ends, and no date is said plainly, never invented.
|
|
31
|
+
*/
|
|
32
|
+
function hwWhen(f) {
|
|
33
|
+
const first = f.first_seen ? f.first_seen.slice(0, 10) : null;
|
|
34
|
+
const last = f.last_seen ? f.last_seen.slice(0, 10) : null;
|
|
35
|
+
if (first && last && last !== first) return ` (first ${first}, last ${last})`;
|
|
36
|
+
if (first) return ` (since ${first})`;
|
|
37
|
+
if (f.basis === 'events' && !/date not recorded/.test(f.title || '')) return ' (date not recorded by the BMC)';
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** HYPERION-NEXT P10: `quox fleet hw <host>`: every finding with what it means and what to do. */
|
|
42
|
+
function printHwHost(r, baseline) {
|
|
43
|
+
console.log(`${r.host || '?'} ${String(r.overall || 'unknown').toUpperCase()} ${r.model || ''} checked ${r.checked_at || '-'}`);
|
|
44
|
+
const c = r.components || {};
|
|
45
|
+
console.log(Object.keys(c).map((k) => `${k} ${c[k]}`).join(' '));
|
|
46
|
+
if (r.dimms) console.log(`memory: ${r.dimms.count} modules, ${r.dimms.total_gb} GB${baseline ? ` (baseline ${baseline.count} modules, ${baseline.total_gb} GB)` : ''}`);
|
|
47
|
+
if (r.power_restore_policy) console.log(`power restore policy: ${r.power_restore_policy}`);
|
|
48
|
+
const findings = r.findings || [];
|
|
49
|
+
if (!findings.length) console.log('\nNo findings.');
|
|
50
|
+
for (const f of findings) {
|
|
51
|
+
console.log(`\n[${f.severity}] ${f.component}: ${f.title}${hwWhen(f)}`);
|
|
52
|
+
console.log(` What it means: ${f.meaning}`);
|
|
53
|
+
console.log(` What to do: ${f.action}`);
|
|
54
|
+
for (const e of (f.evidence || []).slice(0, 3)) console.log(` evidence: ${e}`);
|
|
55
|
+
}
|
|
56
|
+
for (const g of r.gaps || []) printWarning(`not checked: ${g}`);
|
|
57
|
+
}
|
|
13
58
|
|
|
14
59
|
/**
|
|
15
60
|
* Map a raw /api/v1/gpu/nodes row to the flat shape `fleet gpu`'s table
|
|
@@ -95,6 +140,7 @@ export const DEFAULT_CODEX_MODEL = 'gpt-5.4';
|
|
|
95
140
|
|
|
96
141
|
/** Tools whose input schema has a `host` field (collector reads input.host, not body.host_id). */
|
|
97
142
|
const TOOLS_READING_INPUT_HOST = new Set(['ssh_exec']);
|
|
143
|
+
const HW_HOST_RE = /^(?:[A-Za-z0-9][A-Za-z0-9_-]{0,62}|\d{1,3}(?:\.\d{1,3}){3})$/; // same shape the collector's fleet hw routes accept
|
|
98
144
|
|
|
99
145
|
/** Locates the vendored (bwrap-capable) codex binary on the host. */
|
|
100
146
|
export const CODEX_VENDOR_FIND_COMMAND =
|
|
@@ -635,6 +681,121 @@ export function registerFleetCommands(program, ctx) {
|
|
|
635
681
|
}, { format: program.opts().output, emptyMessage: 'No GPU nodes enrolled yet.' });
|
|
636
682
|
});
|
|
637
683
|
|
|
684
|
+
// HYPERION-NEXT P10: bare-metal hardware health (fans, power supplies, temperatures, memory, CPU, RAID, disks).
|
|
685
|
+
const hw = fleet
|
|
686
|
+
.command('hw')
|
|
687
|
+
.description('Hardware health of the bare-metal hosts (fans, PSUs, memory, CPU, RAID, disks); give a host for its findings')
|
|
688
|
+
.argument('[host]', 'one host: list its findings with what they mean and what to do')
|
|
689
|
+
.action(async (host) => {
|
|
690
|
+
auth.requireAuth();
|
|
691
|
+
const json = program.opts().output === 'json';
|
|
692
|
+
if (host) {
|
|
693
|
+
const ep = resolveEndpoint('v1.fleet.hw.get', { host });
|
|
694
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
695
|
+
if (json) { formatDetail(data, { format: 'json' }); return; }
|
|
696
|
+
printHwHost(data.report || {}, data.baseline);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const ep = resolveEndpoint('v1.fleet.hw.list');
|
|
700
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
701
|
+
if (json) { formatDetail(data, { format: 'json' }); return; }
|
|
702
|
+
formatOutput({
|
|
703
|
+
columns: ['host', 'overall', 'fans', 'psu', 'memory', 'cpu', 'raid', 'disks', 'temps', 'findings', 'checked'],
|
|
704
|
+
headers: ['Host', 'Overall', 'Fans', 'PSU', 'Memory', 'CPU', 'RAID', 'Disks', 'Temps', 'Findings', 'Checked'],
|
|
705
|
+
rows: (data.hosts || []).map(formatHwRow),
|
|
706
|
+
}, { format: program.opts().output, emptyMessage: data.no_fleet_for_org ? (data.message || 'This organisation has no fleet hosts on this Quox instance.') : 'No hardware reports yet. Run: quox fleet hw refresh' });
|
|
707
|
+
const o = data.overall || {};
|
|
708
|
+
if ((data.hosts || []).length) printInfo(`${o.fault || 0} fault, ${o.warn || 0} warn, ${o.ok || 0} ok, ${o.unknown || 0} unknown. Details: quox fleet hw <host>`);
|
|
709
|
+
if (data.poller) printInfo(`Poller: ${data.poller.armed ? `on, every ${Math.round((data.poller.interval_ms || 0) / 60000)} min` : 'off (FLEET_HW_POLL=1 turns it on)'}${data.poller.state && data.poller.state.last_outcome ? `, last tick ${data.poller.state.last_outcome}` : ''}`);
|
|
710
|
+
if (data.alerts) printInfo(`Alerts: ${data.alerts.enabled ? 'on, one inbox notification per new fault' : `off (${data.alerts.env_flag || 'FLEET_HW_ALERTS'}=1 turns them on)`}`);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
hw
|
|
714
|
+
.command('events')
|
|
715
|
+
.description('Hardware event log (IPMI SEL) entries across the bare-metal hosts')
|
|
716
|
+
.option('--since <window>', 'only events since this window (30m, 12h, 1d, 2w) or ISO time', '1d')
|
|
717
|
+
.option('--host <host>', 'one host only')
|
|
718
|
+
.option('--severity <level>', 'fault, warn or info')
|
|
719
|
+
.option('--limit <n>', 'most events to return (max 1000)', '200')
|
|
720
|
+
.action(async (options) => {
|
|
721
|
+
auth.requireAuth();
|
|
722
|
+
const ep = resolveEndpoint('v1.fleet.hw.events');
|
|
723
|
+
const query = { since: options.since, limit: options.limit };
|
|
724
|
+
if (options.host) query.host = options.host;
|
|
725
|
+
if (options.severity) query.severity = options.severity;
|
|
726
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
727
|
+
if (program.opts().output === 'json') { formatDetail(data, { format: 'json' }); return; }
|
|
728
|
+
formatOutput({
|
|
729
|
+
columns: ['when', 'host', 'severity', 'sensor', 'event'],
|
|
730
|
+
headers: ['When', 'Host', 'Severity', 'Sensor', 'Event'],
|
|
731
|
+
rows: (data.events || []).map((e) => ({
|
|
732
|
+
when: e.ts ? e.ts.replace('T', ' ').slice(0, 19) : (e.raw_time || '-'),
|
|
733
|
+
host: e.host,
|
|
734
|
+
severity: e.severity,
|
|
735
|
+
sensor: e.sensor || '-',
|
|
736
|
+
event: [e.event, e.direction, e.detail].filter(Boolean).join(' | '),
|
|
737
|
+
})),
|
|
738
|
+
}, { format: program.opts().output, emptyMessage: `No hardware events since ${options.since}.` });
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
hw
|
|
742
|
+
.command('refresh')
|
|
743
|
+
.description('Check the bare-metal hardware now (read-only IPMI, RAID and SMART reads) and store a fresh report')
|
|
744
|
+
.argument('[host]', 'one host only (default: every bare-metal host)')
|
|
745
|
+
.action(async (host) => {
|
|
746
|
+
auth.requireAuth();
|
|
747
|
+
const ep = resolveEndpoint('v1.fleet.hw.refresh');
|
|
748
|
+
const data = await client.post(ep.path, { service: ep.service, body: host ? { hosts: [host] } : {}, timeout: 600000 });
|
|
749
|
+
if (program.opts().output === 'json') { formatDetail(data, { format: 'json' }); return; }
|
|
750
|
+
formatOutput({
|
|
751
|
+
columns: ['host', 'overall', 'faults', 'warns', 'top'],
|
|
752
|
+
headers: ['Host', 'Overall', 'Faults', 'Warnings', 'Top finding'],
|
|
753
|
+
rows: (data.hosts || []).map((h) => ({ host: h.host, overall: h.overall, faults: h.faults ?? 0, warns: h.warns ?? 0, top: (h.findings && h.findings[0] && h.findings[0].title) || '-' })),
|
|
754
|
+
}, { format: program.opts().output, emptyMessage: 'No bare-metal host answered.' });
|
|
755
|
+
for (const u of data.unreachable || []) printWarning(`${u.host}: no hardware answer (${u.error})`);
|
|
756
|
+
printInfo(`${(data.hosts || []).length} host(s) checked, ${data.reads || 0} read(s), ${data.failures || 0} failure(s). Collection ${data.collection_id || '-'}`);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// HYPERION-NEXT P10 slice 2: blink the chassis identify light so a person can find the server in the rack.
|
|
760
|
+
// An ACTION on the BMC, never card-free: it goes through ssh_exec, where `ipmitool chassis identify <n>` is not an
|
|
761
|
+
// allowlisted read, so the call is held for a human approval card like any other fleet write.
|
|
762
|
+
hw
|
|
763
|
+
.command('identify')
|
|
764
|
+
.description('Blink a host\'s chassis identify light so it can be found in the rack (held for approval)')
|
|
765
|
+
.argument('<host>', 'the bare-metal host')
|
|
766
|
+
.option('--seconds <n>', 'how long the light blinks, 30 to 600 seconds', '120')
|
|
767
|
+
.option('--reason <text>', 'why; shown on the approval card')
|
|
768
|
+
.option('--org <id>', 'org ID (overrides active org; required for QUOX_SERVICE_KEY automation callers unless QUOX_ORG_ID is set)')
|
|
769
|
+
.action(async (host, options) => {
|
|
770
|
+
auth.requireAuth();
|
|
771
|
+
if (!HW_HOST_RE.test(String(host))) throw new Error(`not a safe short hostname: ${JSON.stringify(String(host).slice(0, 40))}`);
|
|
772
|
+
const raw = String(options.seconds);
|
|
773
|
+
const seconds = /^\d{1,3}$/.test(raw) ? Number(raw) : NaN;
|
|
774
|
+
if (!(seconds >= 30 && seconds <= 600)) throw new Error(`--seconds must be a whole number from 30 to 600 (got ${JSON.stringify(raw.slice(0, 20))})`);
|
|
775
|
+
const body = {
|
|
776
|
+
tool_name: 'ssh_exec',
|
|
777
|
+
input: { host, command: `ipmitool chassis identify ${seconds}` },
|
|
778
|
+
reason: options.reason ? String(options.reason) : `Blink the identify light on ${host} for ${seconds} seconds so someone can find it in the rack`,
|
|
779
|
+
};
|
|
780
|
+
const orgId = auth.getOrgId(options.org || process.env.QUOX_ORG_ID);
|
|
781
|
+
if (!orgId && process.env.QUOX_SERVICE_KEY) {
|
|
782
|
+
throw new Error('QUOX_SERVICE_KEY is set but no org could be resolved. Set QUOX_ORG_ID=<org> for automation callers, or pass --org <id>.');
|
|
783
|
+
}
|
|
784
|
+
if (orgId) body.org_id = orgId;
|
|
785
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
786
|
+
const data = await client.post(ep.path, { service: ep.service, body, timeout: 120000 });
|
|
787
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); }
|
|
788
|
+
const heldId = data && data.pending && (data.approvalId || (data._approval && data._approval.id));
|
|
789
|
+
const failed = (data && data.success === false) || (data && data.result && data.result.success === false);
|
|
790
|
+
if (program.opts().output !== 'json') {
|
|
791
|
+
if (data && data.pending) printInfo(`Held for approval (${heldId || 'pending'}). Once approved, the identify light on ${host} blinks for ${seconds} seconds.`);
|
|
792
|
+
else if (failed) printWarning(`The identify light on ${host} could not be turned on: ${(data && (data.error || (data.result && data.result.error))) || 'the command failed'}`);
|
|
793
|
+
else console.log(`The identify light on ${host} is blinking for ${seconds} seconds.`);
|
|
794
|
+
}
|
|
795
|
+
if (heldId) console.error(`after approval run: quox approvals result ${heldId}`);
|
|
796
|
+
if (failed && !(data && data.pending)) process.exitCode = 1;
|
|
797
|
+
});
|
|
798
|
+
|
|
638
799
|
fleet
|
|
639
800
|
.command('tools')
|
|
640
801
|
.description('List available fleet tools')
|
|
@@ -716,6 +877,9 @@ export function registerFleetCommands(program, ctx) {
|
|
|
716
877
|
if (failed && !(data && data.pending)) process.exitCode = 1;
|
|
717
878
|
});
|
|
718
879
|
|
|
880
|
+
// BASTION-JANUS P3: the governed fleet recipe library (lib/commands/fleetRecipe.js).
|
|
881
|
+
registerFleetRecipeCommands(fleet, { client, auth, program });
|
|
882
|
+
|
|
719
883
|
fleet
|
|
720
884
|
.command('host-update <hostId>')
|
|
721
885
|
.description('Update a host in the Bastion registry (link/clear its bootstrap credential)')
|