quoxcli 0.0.0-stage → 0.2.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.
- package/LICENSE +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +72 -4
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VOLT commands: verify evidence bundles, inspect chains, export reports
|
|
3
|
+
*
|
|
4
|
+
* quox volt <subcommand> [options]
|
|
5
|
+
*
|
|
6
|
+
* Works with VOLT evidence bundle files (JSON) and raw NDJSON chain exports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
11
|
+
import { printSuccess, printWarning, printInfo, formatDetail } from '../core/output.js';
|
|
12
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Peel a fetched VOLT payload down to the verifiable bundle shape
|
|
16
|
+
* ({ manifest, events_ndjson?, signature? }), not the storage row around it.
|
|
17
|
+
* `/volt/runs/:runId/bundle` returns { bundle: <db row> } with the actual
|
|
18
|
+
* bundle nested in `row.data`; `/volt/bundles/:bundleId/download` returns the
|
|
19
|
+
* bundle directly. Shared by `volt fetch` for both the initial fetch and the
|
|
20
|
+
* follow-up download after `volt.bundle.create` (fleet #447).
|
|
21
|
+
*/
|
|
22
|
+
function unwrapVoltBundle(data) {
|
|
23
|
+
let bundle = (data && data.bundle) ? data.bundle : data;
|
|
24
|
+
if (bundle && bundle.data && typeof bundle.data === 'object' && bundle.data.manifest) {
|
|
25
|
+
bundle = bundle.data;
|
|
26
|
+
}
|
|
27
|
+
return bundle;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerVoltCommands(program, ctx = {}) {
|
|
31
|
+
const { client, auth } = ctx;
|
|
32
|
+
|
|
33
|
+
const volt = program
|
|
34
|
+
.command('volt')
|
|
35
|
+
.description('VOLT evidence bundle verification and inspection');
|
|
36
|
+
|
|
37
|
+
// ── volt verify ─────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
volt
|
|
40
|
+
.command('verify')
|
|
41
|
+
.description('Verify an evidence bundle or NDJSON chain file')
|
|
42
|
+
.argument('<file>', 'path to bundle JSON or events NDJSON file')
|
|
43
|
+
.option('--strict', 'fail on warnings too; unsigned or unpinned signatures fail')
|
|
44
|
+
.option('--key <pubkey|file>', 'trusted Ed25519 public key (base64 raw, or file containing it); repeatable', (v, acc) => (acc || []).concat(v))
|
|
45
|
+
.option('--trusted-keys <file>', 'JSON file: array of base64 public keys or {publicKey, keyId?} objects')
|
|
46
|
+
.action(async (file, options) => {
|
|
47
|
+
const raw = await readFile(file, 'utf8');
|
|
48
|
+
|
|
49
|
+
// Detect if this is a bundle (JSON with manifest) or raw NDJSON
|
|
50
|
+
const isBundle = raw.trimStart().startsWith('{');
|
|
51
|
+
|
|
52
|
+
if (isBundle) {
|
|
53
|
+
// fleet #447: a persisted per-run bundle can be manifest_only (no
|
|
54
|
+
// events_ndjson, see voltContentStore.js). Handing that straight to
|
|
55
|
+
// verifyEvidenceBundle produced a bare MISSING_EVENTS error even
|
|
56
|
+
// though the engine is working correctly, there is simply nothing to
|
|
57
|
+
// chain-verify. Catch it here and name the remedy instead.
|
|
58
|
+
let manifestOnly = null;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
const hasEvents = typeof parsed.events_ndjson === 'string' && parsed.events_ndjson.trim().length > 0;
|
|
62
|
+
if (parsed && parsed.manifest && !hasEvents) {
|
|
63
|
+
manifestOnly = parsed.manifest;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Not valid JSON, let verifyEvidenceBundle report its own PARSE_ERROR below.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (manifestOnly) {
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(chalk.bold('VOLT Evidence Bundle Verification'));
|
|
72
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
73
|
+
console.log('');
|
|
74
|
+
const mode = manifestOnly.bundle_mode ? ` (bundle_mode: ${manifestOnly.bundle_mode})` : '';
|
|
75
|
+
printWarning(`Manifest-only bundle${mode}: no events_ndjson embedded, so there is no chain to verify.`);
|
|
76
|
+
const remedy = manifestOnly.run_id
|
|
77
|
+
? `quox volt fetch ${manifestOnly.run_id}`
|
|
78
|
+
: 'quox volt fetch <run_id>';
|
|
79
|
+
printInfo(`Remedy: ${remedy} to build a verifiable bundle.`);
|
|
80
|
+
console.log('');
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { verifyEvidenceBundle } = await import('../../../src/lib/volt/volt-verify.js');
|
|
86
|
+
const trustedKeys = [];
|
|
87
|
+
const addKey = (k) => {
|
|
88
|
+
if (typeof k === 'string') trustedKeys.push({ publicKey: k.trim() });
|
|
89
|
+
else if (k && k.publicKey) trustedKeys.push({ publicKey: k.publicKey, ...(k.keyId ? { keyId: k.keyId } : {}) });
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
for (const k of options.key || []) {
|
|
93
|
+
let v = k;
|
|
94
|
+
try { v = (await readFile(k, 'utf8')).trim(); } catch { /* literal key */ }
|
|
95
|
+
addKey(v);
|
|
96
|
+
}
|
|
97
|
+
if (options.trustedKeys) {
|
|
98
|
+
const list = JSON.parse(await readFile(options.trustedKeys, 'utf8'));
|
|
99
|
+
(Array.isArray(list) ? list : list.keys || []).forEach(addKey);
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.log(chalk.red(`Could not load trusted keys: ${err.message}`));
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const result = await verifyEvidenceBundle(raw, {
|
|
107
|
+
strict: !!options.strict,
|
|
108
|
+
...(trustedKeys.length ? { keys: trustedKeys } : {})
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.bold('VOLT Evidence Bundle Verification'));
|
|
113
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
114
|
+
|
|
115
|
+
if (result.summary.bundle_id) {
|
|
116
|
+
console.log(` Bundle: ${chalk.cyan(result.summary.bundle_id)}`);
|
|
117
|
+
}
|
|
118
|
+
if (result.summary.run_id) {
|
|
119
|
+
console.log(` Run: ${chalk.cyan(result.summary.run_id)}`);
|
|
120
|
+
}
|
|
121
|
+
console.log(` Version: ${result.summary.volt_version || 'unknown'}`);
|
|
122
|
+
console.log(` Events: ${result.summary.event_count}`);
|
|
123
|
+
console.log(` Chain: ${result.summary.chain_valid ? chalk.green('PASS') : chalk.red('FAIL')}`);
|
|
124
|
+
{
|
|
125
|
+
const sigs = result.summary.signatures || [];
|
|
126
|
+
if (sigs.length === 0) {
|
|
127
|
+
console.log(` Signature: ${chalk.yellow('NONE')} (bundle is unsigned)`);
|
|
128
|
+
}
|
|
129
|
+
for (const sg of sigs) {
|
|
130
|
+
const key = `${String(sg.key_id || 'unknown').slice(0, 4)}...`;
|
|
131
|
+
if (sg.valid) {
|
|
132
|
+
const how = sg.trust === 'trusted' ? 'trusted' : 'embedded, not pinned';
|
|
133
|
+
console.log(` Signature: ${chalk.green('VALID')} (key ${key}, ${how})`);
|
|
134
|
+
} else {
|
|
135
|
+
console.log(` Signature: ${chalk.red('INVALID')} (key ${key}, see errors)`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log(` Timestamps: ${result.summary.timestamps_checked || 0}`);
|
|
140
|
+
|
|
141
|
+
if (result.errors.length > 0) {
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(chalk.red(` ${result.errors.length} error(s):`));
|
|
144
|
+
for (const err of result.errors) {
|
|
145
|
+
console.log(chalk.red(` ✗ [${err.code}] ${err.message}`));
|
|
146
|
+
if (err.detail) console.log(chalk.gray(` ${err.detail}`));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (result.warnings.length > 0) {
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(chalk.yellow(` ${result.warnings.length} warning(s):`));
|
|
152
|
+
for (const w of result.warnings) {
|
|
153
|
+
console.log(chalk.yellow(` ⚠ [${w.code}] ${w.message}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log('');
|
|
158
|
+
const pass = result.valid && (!options.strict || result.warnings.length === 0);
|
|
159
|
+
if (pass) {
|
|
160
|
+
printSuccess(`RESULT: PASS — bundle is valid (${result.summary.event_count} events verified)`);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(chalk.red.bold(` RESULT: FAIL`));
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Raw NDJSON chain
|
|
167
|
+
const { verifyNDJSON, formatReport } = await import('../../../src/lib/volt/volt-verifier.js');
|
|
168
|
+
const result = await verifyNDJSON(raw);
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log(formatReport(result));
|
|
171
|
+
console.log('');
|
|
172
|
+
|
|
173
|
+
if (result.result === 'PASS') {
|
|
174
|
+
printSuccess('Chain verified successfully');
|
|
175
|
+
} else {
|
|
176
|
+
console.log(chalk.red.bold('Chain verification FAILED'));
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── volt inspect ────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
volt
|
|
185
|
+
.command('inspect')
|
|
186
|
+
.description('Inspect events inside a bundle or NDJSON file')
|
|
187
|
+
.argument('<file>', 'path to bundle JSON or events NDJSON file')
|
|
188
|
+
.option('--events', 'list all events')
|
|
189
|
+
.option('--event <seq>', 'show a single event by sequence number', parseInt)
|
|
190
|
+
.option('--manifest', 'show bundle manifest only')
|
|
191
|
+
.action(async (file, options) => {
|
|
192
|
+
const raw = await readFile(file, 'utf8');
|
|
193
|
+
const isBundle = raw.trimStart().startsWith('{');
|
|
194
|
+
|
|
195
|
+
let events;
|
|
196
|
+
let manifest = null;
|
|
197
|
+
|
|
198
|
+
if (isBundle) {
|
|
199
|
+
const parsed = JSON.parse(raw);
|
|
200
|
+
manifest = parsed.manifest;
|
|
201
|
+
events = parsed.events_ndjson
|
|
202
|
+
.trim()
|
|
203
|
+
.split('\n')
|
|
204
|
+
.filter(l => l.trim())
|
|
205
|
+
.map(l => JSON.parse(l));
|
|
206
|
+
} else {
|
|
207
|
+
events = raw.trim().split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --manifest: show just the bundle manifest
|
|
211
|
+
if (options.manifest) {
|
|
212
|
+
if (!manifest) {
|
|
213
|
+
printWarning('Not a bundle file — no manifest available');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
formatDetail(manifest, {
|
|
217
|
+
format: program.opts().output,
|
|
218
|
+
title: `Bundle Manifest: ${manifest.bundle_id || 'unknown'}`,
|
|
219
|
+
fields: [
|
|
220
|
+
'bundle_id', 'run_id', 'volt_version', 'event_count', 'hash_alg',
|
|
221
|
+
'first_event_hash', 'last_event_hash', 'merkle_root',
|
|
222
|
+
'agents_involved', 'run_status', 'started_at', 'completed_at',
|
|
223
|
+
'signed', 'redactions_present', 'created_ts'
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --event <seq>: show a single event
|
|
230
|
+
if (options.event != null) {
|
|
231
|
+
const ev = events.find(e => e.seq === options.event);
|
|
232
|
+
if (!ev) {
|
|
233
|
+
printWarning(`No event with seq=${options.event} (range: 1-${events.length})`);
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.log(JSON.stringify(ev, null, 2));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --events or default: list events summary
|
|
242
|
+
console.log('');
|
|
243
|
+
if (manifest) {
|
|
244
|
+
console.log(chalk.bold(`Bundle: ${manifest.bundle_id || 'N/A'}`));
|
|
245
|
+
console.log(chalk.gray(`Run: ${manifest.run_id || 'N/A'} — ${events.length} events`));
|
|
246
|
+
} else {
|
|
247
|
+
console.log(chalk.bold(`NDJSON Chain — ${events.length} events`));
|
|
248
|
+
if (events.length > 0) {
|
|
249
|
+
console.log(chalk.gray(`Run: ${events[0].run_id || 'N/A'}`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
console.log('');
|
|
253
|
+
|
|
254
|
+
// Table header
|
|
255
|
+
console.log(
|
|
256
|
+
chalk.gray(' SEQ ') +
|
|
257
|
+
chalk.gray('EVENT TYPE'.padEnd(28)) +
|
|
258
|
+
chalk.gray('ACTOR'.padEnd(20)) +
|
|
259
|
+
chalk.gray('HASH (first 12)')
|
|
260
|
+
);
|
|
261
|
+
console.log(chalk.gray(' ' + '─'.repeat(75)));
|
|
262
|
+
|
|
263
|
+
for (const ev of events) {
|
|
264
|
+
const seqStr = String(ev.seq).padStart(4);
|
|
265
|
+
const typeStr = (ev.event_type || 'unknown').padEnd(28);
|
|
266
|
+
const actor = (ev.actor?.actor_id || '—').padEnd(20);
|
|
267
|
+
const hashShort = ev.hash ? ev.hash.slice(0, 12) + '…' : '—';
|
|
268
|
+
|
|
269
|
+
console.log(` ${seqStr} ${typeStr}${actor}${chalk.gray(hashShort)}`);
|
|
270
|
+
}
|
|
271
|
+
console.log('');
|
|
272
|
+
printInfo(`${events.length} events total`);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ── volt export ─────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
volt
|
|
278
|
+
.command('export')
|
|
279
|
+
.description('Re-export a bundle in a different format')
|
|
280
|
+
.argument('<file>', 'path to bundle JSON file')
|
|
281
|
+
.option('--ndjson <outfile>', 'export events as NDJSON')
|
|
282
|
+
.option('--manifest <outfile>', 'export manifest as JSON')
|
|
283
|
+
.option('--pretty', 'pretty-print JSON output')
|
|
284
|
+
.action(async (file, options) => {
|
|
285
|
+
const raw = await readFile(file, 'utf8');
|
|
286
|
+
const parsed = JSON.parse(raw);
|
|
287
|
+
|
|
288
|
+
if (!parsed.manifest || !parsed.events_ndjson) {
|
|
289
|
+
printWarning('File does not appear to be a VOLT evidence bundle');
|
|
290
|
+
process.exitCode = 1;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (options.ndjson) {
|
|
295
|
+
await writeFile(options.ndjson, parsed.events_ndjson);
|
|
296
|
+
printSuccess(`Events exported to ${options.ndjson} (${parsed.manifest.event_count} events)`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (options.manifest) {
|
|
300
|
+
const content = options.pretty
|
|
301
|
+
? JSON.stringify(parsed.manifest, null, 2)
|
|
302
|
+
: JSON.stringify(parsed.manifest);
|
|
303
|
+
await writeFile(options.manifest, content + '\n');
|
|
304
|
+
printSuccess(`Manifest exported to ${options.manifest}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!options.ndjson && !options.manifest) {
|
|
308
|
+
printInfo('Specify --ndjson <outfile> and/or --manifest <outfile> to export');
|
|
309
|
+
console.log('');
|
|
310
|
+
console.log('Examples:');
|
|
311
|
+
console.log(' quox volt export bundle.json --ndjson events.ndjson');
|
|
312
|
+
console.log(' quox volt export bundle.json --manifest manifest.json --pretty');
|
|
313
|
+
console.log(' quox volt export bundle.json --ndjson events.ndjson --manifest manifest.json');
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ── volt fetch ──────────────────────────────────────────────────────────────
|
|
318
|
+
//
|
|
319
|
+
// 2026-08-19 (proof-loop). `quox volt verify` only accepts a LOCAL FILE, and
|
|
320
|
+
// no verb existed to obtain one from the running instance. The collector has
|
|
321
|
+
// served /volt/runs/:runId/bundle and /volt/bundles/:bundleId/download all
|
|
322
|
+
// along with nothing wired to them, so the chain a customer needs —
|
|
323
|
+
// run -> bundle -> verify — was broken at exactly the step where they check
|
|
324
|
+
// a receipt for themselves. That is the "keep the receipt" promise, and it
|
|
325
|
+
// could not be kept from the canonical CLI.
|
|
326
|
+
|
|
327
|
+
volt
|
|
328
|
+
.command('fetch')
|
|
329
|
+
.description('Download a run\'s evidence bundle so `quox volt verify` can check it')
|
|
330
|
+
.argument('<runId>', 'VOLT run id (see `quox governance cert list`)')
|
|
331
|
+
.option('--out <file>', 'write the bundle here (default: <runId>-bundle.json)')
|
|
332
|
+
.option('--bundle-id <id>', 'fetch a specific bundle id instead of the run\'s current one')
|
|
333
|
+
.option('--manifest-only', 'write the persisted manifest-only bundle as-is, skip building a verifiable one')
|
|
334
|
+
.action(async (runId, options) => {
|
|
335
|
+
auth.requireAuth();
|
|
336
|
+
|
|
337
|
+
const ep = options.bundleId
|
|
338
|
+
? resolveEndpoint('volt.bundle.download', { bundleId: options.bundleId })
|
|
339
|
+
: resolveEndpoint('volt.run.bundle.get', { runId });
|
|
340
|
+
|
|
341
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
342
|
+
|
|
343
|
+
// Unwrap to the VERIFIABLE bundle, not the storage row around it.
|
|
344
|
+
//
|
|
345
|
+
// /volt/runs/:runId/bundle returns { bundle: <db row> } where the row is
|
|
346
|
+
// { bundle_id, run_id, created_ts, data, stored_at } and the thing
|
|
347
|
+
// `quox volt verify` actually needs (manifest + events_ndjson +
|
|
348
|
+
// signatures) is nested in `data`. Writing the row produced a file that
|
|
349
|
+
// failed verification with MISSING_MANIFEST — the verifier was right and
|
|
350
|
+
// the fetch was wrong. Peel both layers, and only accept the result if it
|
|
351
|
+
// carries a manifest, so this can never hand someone a file that looks
|
|
352
|
+
// like evidence and is not.
|
|
353
|
+
let bundle = unwrapVoltBundle(data);
|
|
354
|
+
if (!bundle) {
|
|
355
|
+
printWarning(`No bundle found for ${options.bundleId || runId}`);
|
|
356
|
+
process.exitCode = 1;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!bundle.manifest) {
|
|
360
|
+
printWarning(`Fetched payload for ${options.bundleId || runId} has no manifest — refusing to write a file that would fail verification.`);
|
|
361
|
+
printInfo(`Payload keys: ${Object.keys(bundle).join(', ')}`);
|
|
362
|
+
process.exitCode = 1;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const hasEvents = typeof bundle.events_ndjson === 'string' && bundle.events_ndjson.trim().length > 0;
|
|
367
|
+
|
|
368
|
+
// fleet #447: the bundle persisted at run completion is bundle_mode:
|
|
369
|
+
// 'manifest_only' (see voltContentStore.js), so a plain fetch handed
|
|
370
|
+
// `quox volt verify` a file it could never chain-verify. Build the full
|
|
371
|
+
// bundle on demand instead, unless the caller asked for a specific
|
|
372
|
+
// bundle id (that bundle is what it is) or opted out with
|
|
373
|
+
// --manifest-only.
|
|
374
|
+
if (!hasEvents) {
|
|
375
|
+
if (options.bundleId) {
|
|
376
|
+
const out = options.out || `${options.bundleId}-bundle.json`;
|
|
377
|
+
await writeFile(out, JSON.stringify(bundle, null, 2) + '\n');
|
|
378
|
+
const mode = bundle.manifest.bundle_mode ? ` (bundle_mode: ${bundle.manifest.bundle_mode})` : '';
|
|
379
|
+
printSuccess(`Bundle written to ${out}`);
|
|
380
|
+
printWarning(`Bundle ${options.bundleId} is manifest-only${mode}, no events_ndjson to verify.`);
|
|
381
|
+
const runHint = bundle.manifest.run_id;
|
|
382
|
+
printInfo(runHint ? `For a verifiable bundle: quox volt fetch ${runHint}` : 'Fetch by run id for a verifiable bundle.');
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (options.manifestOnly) {
|
|
387
|
+
const out = options.out || `${runId}-bundle.json`;
|
|
388
|
+
await writeFile(out, JSON.stringify(bundle, null, 2) + '\n');
|
|
389
|
+
printSuccess(`Bundle written to ${out}`);
|
|
390
|
+
printWarning('This is a manifest-only bundle: `quox volt verify` cannot chain-verify it (no events_ndjson).');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
printInfo(`Persisted bundle for ${runId} is manifest-only, building a verifiable bundle from stored events.`);
|
|
395
|
+
|
|
396
|
+
const createEp = resolveEndpoint('volt.bundle.create');
|
|
397
|
+
const created = await client.post(createEp.path, { service: createEp.service, body: { run_id: runId } });
|
|
398
|
+
const bundleId = created && created.bundle_id;
|
|
399
|
+
if (!bundleId) {
|
|
400
|
+
printWarning(`Could not build a verifiable bundle for ${runId}.`);
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const downloadEp = resolveEndpoint('volt.bundle.download', { bundleId });
|
|
406
|
+
const fullData = await client.get(downloadEp.path, { service: downloadEp.service });
|
|
407
|
+
const fullBundle = unwrapVoltBundle(fullData);
|
|
408
|
+
if (!fullBundle || !fullBundle.manifest) {
|
|
409
|
+
printWarning(`Built bundle ${bundleId} but could not download it, no manifest in the response.`);
|
|
410
|
+
process.exitCode = 1;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const out = options.out || `${runId}-bundle.json`;
|
|
415
|
+
await writeFile(out, JSON.stringify(fullBundle, null, 2) + '\n');
|
|
416
|
+
const eventCount = fullBundle.manifest.event_count;
|
|
417
|
+
printSuccess(`Bundle ${bundleId} written to ${out}${eventCount != null ? ` (${eventCount} events)` : ''}`);
|
|
418
|
+
printInfo(`Verify it: quox volt verify ${out}`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const out = options.out || `${options.bundleId || runId}-bundle.json`;
|
|
423
|
+
await writeFile(out, JSON.stringify(bundle, null, 2) + '\n');
|
|
424
|
+
|
|
425
|
+
const events = bundle.manifest && bundle.manifest.event_count;
|
|
426
|
+
printSuccess(`Bundle written to ${out}${events != null ? ` (${events} events)` : ''}`);
|
|
427
|
+
printInfo(`Verify it: quox volt verify ${out}`);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// ── volt summary ────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
volt
|
|
433
|
+
.command('summary')
|
|
434
|
+
.description('Quick summary of a bundle (no verification)')
|
|
435
|
+
.argument('<file>', 'path to bundle JSON file')
|
|
436
|
+
.action(async (file) => {
|
|
437
|
+
const raw = await readFile(file, 'utf8');
|
|
438
|
+
const parsed = JSON.parse(raw);
|
|
439
|
+
|
|
440
|
+
if (!parsed.manifest) {
|
|
441
|
+
printWarning('File does not appear to be a VOLT evidence bundle');
|
|
442
|
+
process.exitCode = 1;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const m = parsed.manifest;
|
|
447
|
+
const events = parsed.events_ndjson
|
|
448
|
+
?.trim()
|
|
449
|
+
.split('\n')
|
|
450
|
+
.filter(l => l.trim()) || [];
|
|
451
|
+
|
|
452
|
+
// Event type breakdown
|
|
453
|
+
const typeCounts = {};
|
|
454
|
+
for (const line of events) {
|
|
455
|
+
try {
|
|
456
|
+
const ev = JSON.parse(line);
|
|
457
|
+
typeCounts[ev.event_type] = (typeCounts[ev.event_type] || 0) + 1;
|
|
458
|
+
} catch { /* skip */ }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
console.log('');
|
|
462
|
+
console.log(chalk.bold('VOLT Bundle Summary'));
|
|
463
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
464
|
+
console.log(` Bundle ID: ${chalk.cyan(m.bundle_id || 'N/A')}`);
|
|
465
|
+
console.log(` Run ID: ${chalk.cyan(m.run_id || 'N/A')}`);
|
|
466
|
+
console.log(` Version: ${m.volt_version || 'N/A'}`);
|
|
467
|
+
console.log(` Status: ${m.run_status || 'N/A'}`);
|
|
468
|
+
console.log(` Events: ${m.event_count || events.length}`);
|
|
469
|
+
console.log(` Agents: ${(m.agents_involved || []).join(', ') || 'none'}`);
|
|
470
|
+
console.log(` Policies: ${(m.policy_ids_evaluated || []).length} evaluated`);
|
|
471
|
+
console.log(` HITL: ${m.hitl_decisions || 0} decisions`);
|
|
472
|
+
console.log(` Signed: ${m.signed ? 'yes' : 'no'}`);
|
|
473
|
+
console.log(` Redacted: ${m.redactions_present ? 'yes' : 'no'}`);
|
|
474
|
+
console.log(` Created: ${m.created_ts || 'N/A'}`);
|
|
475
|
+
console.log(` Started: ${m.started_at || 'N/A'}`);
|
|
476
|
+
console.log(` Completed: ${m.completed_at || 'N/A'}`);
|
|
477
|
+
|
|
478
|
+
if (Object.keys(typeCounts).length > 0) {
|
|
479
|
+
console.log('');
|
|
480
|
+
console.log(chalk.bold(' Event Types:'));
|
|
481
|
+
const sorted = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
|
|
482
|
+
for (const [type, count] of sorted) {
|
|
483
|
+
console.log(` ${String(count).padStart(4)} ${type}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
console.log('');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// ── volt accept / acceptance ────────────────────────────────────────────────
|
|
490
|
+
// SEO-MOAT (owner ruling 2026-09-03): the "did it work" layer. The chain
|
|
491
|
+
// proves what happened; this records whether it achieved its intent — an
|
|
492
|
+
// attestation, not automated evaluation. The server derives WHO attested
|
|
493
|
+
// from the credential class (human/agent/service) and witnesses the
|
|
494
|
+
// verdict as its own VOLT run pinning the target run's head_hash, so an
|
|
495
|
+
// agent marking its own work "pass" is visible as exactly that.
|
|
496
|
+
|
|
497
|
+
volt
|
|
498
|
+
.command('accept')
|
|
499
|
+
.description('Attest a terminal run\'s outcome: did it achieve its intent?')
|
|
500
|
+
.argument('<runId>', 'VOLT run id (VR_...) — see `quox volt fetch`')
|
|
501
|
+
.requiredOption('--verdict <verdict>', "'pass' or 'fail'")
|
|
502
|
+
.option('--reason <text>', 'what you checked and what you saw')
|
|
503
|
+
.action(async (runId, options) => {
|
|
504
|
+
auth.requireAuth();
|
|
505
|
+
const ep = resolveEndpoint('volt.run.acceptance.post', { runId });
|
|
506
|
+
const data = await client.post(ep.path, {
|
|
507
|
+
service: ep.service,
|
|
508
|
+
body: { verdict: options.verdict, reason: options.reason || null },
|
|
509
|
+
});
|
|
510
|
+
const v = data.verdict || {};
|
|
511
|
+
printSuccess(`Acceptance recorded: ${v.verdict} on ${runId} (attested by ${v.actor_type}.${v.actor_id})`);
|
|
512
|
+
if (v.evidence_run_id) printInfo(`Witnessed as VOLT run ${v.evidence_run_id} (pins target head_hash ${String(v.target_head_hash || '').slice(0, 12)}...)`);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
volt
|
|
516
|
+
.command('acceptance')
|
|
517
|
+
.description('Show a run\'s acceptance verdict history (latest is the standing verdict)')
|
|
518
|
+
.argument('<runId>', 'VOLT run id (VR_...)')
|
|
519
|
+
.action(async (runId) => {
|
|
520
|
+
auth.requireAuth();
|
|
521
|
+
const ep = resolveEndpoint('volt.run.acceptance.get', { runId });
|
|
522
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
523
|
+
formatDetail(data.latest, { format: program.opts().output });
|
|
524
|
+
if ((data.history || []).length > 1) {
|
|
525
|
+
printInfo(`${data.history.length} verdicts total — latest supersedes; history is append-only.`);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch commands — real-time monitoring dashboards
|
|
3
|
+
*
|
|
4
|
+
* quox watch fleet Live fleet status (polling)
|
|
5
|
+
* quox watch services Live service health
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
10
|
+
|
|
11
|
+
export function registerWatchCommands(program, { client, auth, config }) {
|
|
12
|
+
const watch = program
|
|
13
|
+
.command('watch')
|
|
14
|
+
.description('Real-time monitoring dashboards');
|
|
15
|
+
|
|
16
|
+
watch
|
|
17
|
+
.command('fleet')
|
|
18
|
+
.description('Live fleet status dashboard')
|
|
19
|
+
.option('--interval <seconds>', 'Refresh interval', '5')
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
auth.requireAuth();
|
|
22
|
+
const interval = Math.max(2, parseInt(opts.interval, 10) || 5) * 1000;
|
|
23
|
+
|
|
24
|
+
console.log(chalk.cyan('Fleet Watch') + chalk.dim(' (Ctrl+C to stop)\n'));
|
|
25
|
+
|
|
26
|
+
const tick = async () => {
|
|
27
|
+
try {
|
|
28
|
+
const data = await client.get(resolveEndpoint('v1.fleet.status.list').path, { service: resolveEndpoint('v1.fleet.status.list').service });
|
|
29
|
+
const agents = data.agents || [];
|
|
30
|
+
|
|
31
|
+
// Clear screen and redraw
|
|
32
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
33
|
+
console.log(chalk.cyan.bold('QuoxCLI Fleet Watch') + chalk.dim(` ${new Date().toLocaleTimeString()} (Ctrl+C to stop)`));
|
|
34
|
+
console.log(chalk.dim('─'.repeat(72)));
|
|
35
|
+
|
|
36
|
+
if (agents.length === 0) {
|
|
37
|
+
console.log(chalk.yellow(' No agents reporting'));
|
|
38
|
+
} else {
|
|
39
|
+
const header = ` ${'HOST'.padEnd(16)} ${'STATUS'.padEnd(10)} ${'CPU'.padEnd(7)} ${'MEM'.padEnd(7)} ${'UPTIME'.padEnd(10)} ${'VERSION'}`;
|
|
40
|
+
console.log(chalk.dim(header));
|
|
41
|
+
console.log(chalk.dim(' ' + '─'.repeat(68)));
|
|
42
|
+
|
|
43
|
+
for (const a of agents) {
|
|
44
|
+
const statusColor = a.status === 'healthy' ? chalk.green : a.status === 'degraded' ? chalk.yellow : chalk.red;
|
|
45
|
+
const cpuColor = (a.cpu_percent || 0) > 80 ? chalk.red : (a.cpu_percent || 0) > 60 ? chalk.yellow : chalk.white;
|
|
46
|
+
const memColor = (a.memory_percent || 0) > 80 ? chalk.red : (a.memory_percent || 0) > 60 ? chalk.yellow : chalk.white;
|
|
47
|
+
|
|
48
|
+
console.log(
|
|
49
|
+
` ${chalk.white(String(a.host_id || '').padEnd(16))} ` +
|
|
50
|
+
`${statusColor(String(a.status || 'unknown').padEnd(10))} ` +
|
|
51
|
+
`${cpuColor(String((a.cpu_percent || 0) + '%').padEnd(7))} ` +
|
|
52
|
+
`${memColor(String((a.memory_percent || 0) + '%').padEnd(7))} ` +
|
|
53
|
+
`${String(a.uptime || '-').padEnd(10)} ` +
|
|
54
|
+
`${chalk.dim(a.version || '-')}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(chalk.dim('\n ' + '─'.repeat(68)));
|
|
60
|
+
console.log(chalk.dim(` ${agents.length} agents | refreshing every ${interval / 1000}s`));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await tick();
|
|
67
|
+
const timer = setInterval(tick, interval);
|
|
68
|
+
process.on('SIGINT', () => { clearInterval(timer); process.exit(0); });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
watch
|
|
72
|
+
.command('services')
|
|
73
|
+
.description('Live service health dashboard')
|
|
74
|
+
.option('--interval <seconds>', 'Refresh interval', '10')
|
|
75
|
+
.action(async (opts) => {
|
|
76
|
+
const interval = Math.max(5, parseInt(opts.interval, 10) || 10) * 1000;
|
|
77
|
+
const services = [
|
|
78
|
+
{ name: 'auth', port: 3101, path: '/health' },
|
|
79
|
+
{ name: 'collector', port: 9848, path: '/health' },
|
|
80
|
+
{ name: 'memory', port: 3103, path: '/health' },
|
|
81
|
+
{ name: 'files', port: 3102, path: '/health' },
|
|
82
|
+
{ name: 'orchestrator', port: 3100, path: '/health' },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const tick = async () => {
|
|
86
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
87
|
+
console.log(chalk.cyan.bold('QuoxCLI Service Watch') + chalk.dim(` ${new Date().toLocaleTimeString()} (Ctrl+C to stop)`));
|
|
88
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
89
|
+
|
|
90
|
+
for (const svc of services) {
|
|
91
|
+
try {
|
|
92
|
+
const svcUrl = config.getServiceUrl(svc.name);
|
|
93
|
+
const fullUrl = new URL(svc.path, svcUrl);
|
|
94
|
+
const start = Date.now();
|
|
95
|
+
const res = await fetch(fullUrl.href, { signal: AbortSignal.timeout(5000) });
|
|
96
|
+
const latency = Date.now() - start;
|
|
97
|
+
|
|
98
|
+
if (res.ok) {
|
|
99
|
+
console.log(` ${chalk.green('●')} ${svc.name.padEnd(14)} ${chalk.green('healthy')} ${chalk.dim(latency + 'ms')}`);
|
|
100
|
+
} else {
|
|
101
|
+
console.log(` ${chalk.yellow('●')} ${svc.name.padEnd(14)} ${chalk.yellow(res.status)} ${chalk.dim(latency + 'ms')}`);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
console.log(` ${chalk.red('●')} ${svc.name.padEnd(14)} ${chalk.red('down')}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(chalk.dim('\n refreshing every ' + (interval / 1000) + 's'));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
await tick();
|
|
112
|
+
const timer = setInterval(tick, interval);
|
|
113
|
+
process.on('SIGINT', () => { clearInterval(timer); process.exit(0); });
|
|
114
|
+
});
|
|
115
|
+
}
|