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,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governance commands: policies, WARD tamper-evident chain, VOLT evidence
|
|
3
|
+
* bundles / certification, evidence + trace queries.
|
|
4
|
+
*
|
|
5
|
+
* All endpoints below are wired to real, existing collector routes — see
|
|
6
|
+
* the comment above each subcommand for the exact route it calls.
|
|
7
|
+
*
|
|
8
|
+
* Read-only operations (list/get/status/verify/query) are first-class.
|
|
9
|
+
*
|
|
10
|
+
* Policy WRITE operations (create/update/delete/validate, template packs,
|
|
11
|
+
* approval resolution) were added in WP4.2 (STREAM_CLI_PARITY_PLAN.md §2.3
|
|
12
|
+
* #9) — see the `governance policy`, `governance template`, and
|
|
13
|
+
* `governance approval` subcommands below. `governance certify` remains
|
|
14
|
+
* the one action gated behind an explicit --confirm flag (it creates a
|
|
15
|
+
* permanent VOLT certification record, matching the pattern used by
|
|
16
|
+
* `vault export` and `vault delete`); the policy mutations below do not
|
|
17
|
+
* require --confirm because the backend already enforces its own
|
|
18
|
+
* second-approver (4-eyes) workflow for sensitive changes (a 202 response
|
|
19
|
+
* with `pending_approval` instead of applying immediately).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { formatOutput, formatDetail, printSuccess, printWarning } from '../core/output.js';
|
|
23
|
+
import { resolveOrgId, parseJSON } from './helpers.js';
|
|
24
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
25
|
+
|
|
26
|
+
export function registerGovernanceCommands(program, ctx) {
|
|
27
|
+
const { client, auth } = ctx;
|
|
28
|
+
|
|
29
|
+
const governance = program
|
|
30
|
+
.command('governance')
|
|
31
|
+
.description('Governance: policies, WARD chain, VOLT evidence + certification');
|
|
32
|
+
|
|
33
|
+
// ── governance policy ───────────────────────────────────────────────────────
|
|
34
|
+
// GET /api/v1/policies?org_id=... (services/collector/server.js)
|
|
35
|
+
|
|
36
|
+
const policy = governance
|
|
37
|
+
.command('policy')
|
|
38
|
+
.description('Manage governance policies');
|
|
39
|
+
|
|
40
|
+
policy
|
|
41
|
+
.command('list')
|
|
42
|
+
.description('List governance policies')
|
|
43
|
+
.option('--category <category>', 'filter by category')
|
|
44
|
+
.option('--enabled <bool>', 'filter by enabled state (true|false)')
|
|
45
|
+
.option('--effect <effect>', 'filter by effect')
|
|
46
|
+
.option('--limit <n>', 'max results', '100')
|
|
47
|
+
.option('--offset <n>', 'result offset', '0')
|
|
48
|
+
.action(async (options) => {
|
|
49
|
+
auth.requireAuth();
|
|
50
|
+
const orgId = resolveOrgId(auth, program);
|
|
51
|
+
const query = { org_id: orgId, limit: options.limit, offset: options.offset };
|
|
52
|
+
if (options.category) query.category = options.category;
|
|
53
|
+
if (options.enabled !== undefined) query.enabled = options.enabled;
|
|
54
|
+
if (options.effect) query.effect = options.effect;
|
|
55
|
+
|
|
56
|
+
const data = await client.get(resolveEndpoint('v1.policies.list').path, { service: resolveEndpoint('v1.policies.list').service, query });
|
|
57
|
+
formatOutput({
|
|
58
|
+
columns: ['id', 'name', 'category', 'effect', 'enabled', 'updated_at'],
|
|
59
|
+
headers: ['ID', 'Name', 'Category', 'Effect', 'Enabled', 'Updated'],
|
|
60
|
+
rows: data.policies || data.data || data,
|
|
61
|
+
}, { format: program.opts().output, emptyMessage: 'No policies found.' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// GET /api/v1/policies/:id
|
|
65
|
+
policy
|
|
66
|
+
.command('get')
|
|
67
|
+
.description('Show policy details')
|
|
68
|
+
.argument('<id>', 'policy ID')
|
|
69
|
+
.action(async (id) => {
|
|
70
|
+
auth.requireAuth();
|
|
71
|
+
const data = await client.get(resolveEndpoint('v1.policies.get', { id }).path, { service: resolveEndpoint('v1.policies.get', { id }).service });
|
|
72
|
+
formatDetail(data.policy || data, { format: program.opts().output, title: `Policy: ${id}` });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// GET /api/v1/policies/approvals/pending?org_id=...
|
|
76
|
+
policy
|
|
77
|
+
.command('pending-approvals')
|
|
78
|
+
.description('List policy changes awaiting second-approver sign-off')
|
|
79
|
+
.action(async () => {
|
|
80
|
+
auth.requireAuth();
|
|
81
|
+
const orgId = resolveOrgId(auth, program);
|
|
82
|
+
const data = await client.get(resolveEndpoint('v1.policies.approvals.pending.list').path, {
|
|
83
|
+
service: resolveEndpoint('v1.policies.approvals.pending.list').service,
|
|
84
|
+
query: { org_id: orgId },
|
|
85
|
+
});
|
|
86
|
+
formatOutput({
|
|
87
|
+
columns: ['id', 'policy_id', 'operation', 'actor_id', 'created_at'],
|
|
88
|
+
headers: ['ID', 'Policy', 'Operation', 'Requested By', 'Requested'],
|
|
89
|
+
rows: data.data || data,
|
|
90
|
+
}, { format: program.opts().output, emptyMessage: 'No pending approvals.' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// POST /api/v1/policies — create a new policy (WRITE, WP4.2)
|
|
94
|
+
policy
|
|
95
|
+
.command('create')
|
|
96
|
+
.description('Create a new governance policy')
|
|
97
|
+
.requiredOption('--name <name>', 'policy name')
|
|
98
|
+
.requiredOption('--category <category>', 'policy category')
|
|
99
|
+
.option('--effect <effect>', 'allow | deny | require_approval | redact | log_only', 'deny')
|
|
100
|
+
.option('--priority <n>', 'priority (lower runs first)')
|
|
101
|
+
.option('--scope <json>', 'scope as JSON, e.g. \'{"agents":["*"]}\'')
|
|
102
|
+
.option('--conditions <json>', 'conditions as JSON')
|
|
103
|
+
.option('--enabled <bool>', 'enabled state (true|false)', 'true')
|
|
104
|
+
.option('--reason <text>', 'reason for the policy')
|
|
105
|
+
.option('--framework-mapping <json>', 'framework mapping as a JSON array')
|
|
106
|
+
.option('--audit-required', 'flag this policy as requiring audit')
|
|
107
|
+
.action(async (options) => {
|
|
108
|
+
auth.requireAuth();
|
|
109
|
+
const orgId = resolveOrgId(auth, program);
|
|
110
|
+
const body = {
|
|
111
|
+
org_id: orgId,
|
|
112
|
+
name: options.name,
|
|
113
|
+
category: options.category,
|
|
114
|
+
effect: options.effect,
|
|
115
|
+
enabled: options.enabled === undefined ? true : options.enabled === 'true',
|
|
116
|
+
};
|
|
117
|
+
if (options.priority !== undefined) body.priority = parseInt(options.priority, 10);
|
|
118
|
+
if (options.scope) body.scope = parseJSON(options.scope, '--scope');
|
|
119
|
+
if (options.conditions) body.conditions = parseJSON(options.conditions, '--conditions');
|
|
120
|
+
if (options.reason) body.reason = options.reason;
|
|
121
|
+
if (options.frameworkMapping) body.framework_mapping = parseJSON(options.frameworkMapping, '--framework-mapping');
|
|
122
|
+
if (options.auditRequired) body.audit_required = true;
|
|
123
|
+
|
|
124
|
+
const data = await client.post(resolveEndpoint('v1.policies.create').path, { service: resolveEndpoint('v1.policies.create').service, body });
|
|
125
|
+
printSuccess(`Policy "${options.name}" created (id: ${data.policy?.id}).`);
|
|
126
|
+
formatDetail(data.policy || data, { format: program.opts().output });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// PATCH /api/v1/policies/:id — update a policy (WRITE, WP4.2)
|
|
130
|
+
// May return 202 { pending_approval } if the org has opted into 4-eyes
|
|
131
|
+
// review for this change (--enforce-4eyes, or org-level policy).
|
|
132
|
+
policy
|
|
133
|
+
.command('update')
|
|
134
|
+
.description('Update a governance policy (may require second-approver sign-off)')
|
|
135
|
+
.argument('<id>', 'policy ID')
|
|
136
|
+
.option('--name <name>', 'new name')
|
|
137
|
+
.option('--category <category>', 'new category')
|
|
138
|
+
.option('--effect <effect>', 'new effect: allow | deny | require_approval | redact | log_only')
|
|
139
|
+
.option('--priority <n>', 'new priority')
|
|
140
|
+
.option('--scope <json>', 'new scope as JSON')
|
|
141
|
+
.option('--conditions <json>', 'new conditions as JSON')
|
|
142
|
+
.option('--enabled <bool>', 'new enabled state (true|false)')
|
|
143
|
+
.option('--reason <text>', 'reason for the change')
|
|
144
|
+
.option('--enforce-4eyes', 'require second-approver sign-off for this change')
|
|
145
|
+
.action(async (id, options) => {
|
|
146
|
+
auth.requireAuth();
|
|
147
|
+
const body = {};
|
|
148
|
+
if (options.name) body.name = options.name;
|
|
149
|
+
if (options.category) body.category = options.category;
|
|
150
|
+
if (options.effect) body.effect = options.effect;
|
|
151
|
+
if (options.priority !== undefined) body.priority = parseInt(options.priority, 10);
|
|
152
|
+
if (options.scope) body.scope = parseJSON(options.scope, '--scope');
|
|
153
|
+
if (options.conditions) body.conditions = parseJSON(options.conditions, '--conditions');
|
|
154
|
+
if (options.enabled !== undefined) body.enabled = options.enabled === 'true';
|
|
155
|
+
if (options.reason) body.reason_for_change = options.reason;
|
|
156
|
+
if (options.enforce4eyes) body.enforce_4eyes = true;
|
|
157
|
+
|
|
158
|
+
const data = await client.patch(resolveEndpoint('v1.policies.update', { id }).path, { service: resolveEndpoint('v1.policies.update', { id }).service, body });
|
|
159
|
+
if (data.pending_approval) {
|
|
160
|
+
printWarning('Change staged; awaiting second-approver review.');
|
|
161
|
+
formatDetail(data.pending_approval, { format: program.opts().output, title: 'Pending Approval' });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
printSuccess('Policy updated.');
|
|
165
|
+
formatDetail(data.policy || data, { format: program.opts().output });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// DELETE /api/v1/policies/:id — delete a policy (WRITE, WP4.2)
|
|
169
|
+
// May also return 202 { pending_approval } under 4-eyes review.
|
|
170
|
+
policy
|
|
171
|
+
.command('delete')
|
|
172
|
+
.description('Delete a governance policy')
|
|
173
|
+
.argument('<id>', 'policy ID')
|
|
174
|
+
.action(async (id) => {
|
|
175
|
+
auth.requireAuth();
|
|
176
|
+
const data = await client.delete(resolveEndpoint('v1.policies.delete', { id }).path, { service: resolveEndpoint('v1.policies.delete', { id }).service });
|
|
177
|
+
if (data.pending_approval) {
|
|
178
|
+
printWarning('Delete staged; awaiting second-approver review.');
|
|
179
|
+
formatDetail(data.pending_approval, { format: program.opts().output, title: 'Pending Approval' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
printSuccess(`Policy ${id} deleted.`);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// POST /api/v1/policies/validate — dry-run validation, does not persist (WP4.2)
|
|
186
|
+
policy
|
|
187
|
+
.command('validate')
|
|
188
|
+
.description('Validate a policy definition without saving (dry run)')
|
|
189
|
+
.requiredOption('--name <name>', 'policy name')
|
|
190
|
+
.requiredOption('--category <category>', 'policy category')
|
|
191
|
+
.option('--effect <effect>', 'allow | deny | require_approval | redact | log_only', 'deny')
|
|
192
|
+
.option('--priority <n>', 'priority')
|
|
193
|
+
.option('--scope <json>', 'scope as JSON')
|
|
194
|
+
.option('--conditions <json>', 'conditions as JSON')
|
|
195
|
+
.option('--enabled <bool>', 'enabled state (true|false)', 'true')
|
|
196
|
+
.action(async (options) => {
|
|
197
|
+
auth.requireAuth();
|
|
198
|
+
const body = {
|
|
199
|
+
name: options.name,
|
|
200
|
+
category: options.category,
|
|
201
|
+
effect: options.effect,
|
|
202
|
+
enabled: options.enabled === undefined ? true : options.enabled === 'true',
|
|
203
|
+
};
|
|
204
|
+
if (options.priority !== undefined) body.priority = parseInt(options.priority, 10);
|
|
205
|
+
if (options.scope) body.scope = parseJSON(options.scope, '--scope');
|
|
206
|
+
if (options.conditions) body.conditions = parseJSON(options.conditions, '--conditions');
|
|
207
|
+
|
|
208
|
+
const data = await client.post(resolveEndpoint('v1.policies.validate').path, { service: resolveEndpoint('v1.policies.validate').service, body });
|
|
209
|
+
formatDetail(data, { format: program.opts().output, title: 'Validation Result' });
|
|
210
|
+
if (data.valid === false && program.opts().output !== 'json') {
|
|
211
|
+
printWarning('Validation failed.');
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// GET /api/v1/policies/history and GET /api/v1/policies/:id/history (WP4.2)
|
|
216
|
+
policy
|
|
217
|
+
.command('history')
|
|
218
|
+
.description('Show policy change history (org-wide, or a single policy with --policy)')
|
|
219
|
+
.option('--policy <id>', 'show history for a single policy ID instead of the whole org')
|
|
220
|
+
.option('--operation <op>', 'filter by operation: created | updated | deleted')
|
|
221
|
+
.option('--actor <id>', 'filter by actor ID')
|
|
222
|
+
.option('--since <iso>', 'filter: since ISO timestamp')
|
|
223
|
+
.option('--until <iso>', 'filter: until ISO timestamp')
|
|
224
|
+
.option('--limit <n>', 'max results')
|
|
225
|
+
.option('--offset <n>', 'result offset')
|
|
226
|
+
.action(async (options) => {
|
|
227
|
+
auth.requireAuth();
|
|
228
|
+
const columns = ['id', 'policy_id', 'operation', 'actor_id', 'created_at'];
|
|
229
|
+
const headers = ['ID', 'Policy', 'Operation', 'Actor', 'Changed'];
|
|
230
|
+
|
|
231
|
+
if (options.policy) {
|
|
232
|
+
const ep = resolveEndpoint('v1.policies.history.get', { id: options.policy });
|
|
233
|
+
const query = {};
|
|
234
|
+
if (options.limit) query.limit = options.limit;
|
|
235
|
+
if (options.offset) query.offset = options.offset;
|
|
236
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
237
|
+
formatOutput({ columns, headers, rows: data.data || data }, { format: program.opts().output, emptyMessage: 'No history found for this policy.' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const orgId = resolveOrgId(auth, program);
|
|
242
|
+
const ep = resolveEndpoint('v1.policies.history.list');
|
|
243
|
+
const query = { org_id: orgId };
|
|
244
|
+
if (options.operation) query.operation = options.operation;
|
|
245
|
+
if (options.actor) query.actor_id = options.actor;
|
|
246
|
+
if (options.since) query.since = options.since;
|
|
247
|
+
if (options.until) query.until = options.until;
|
|
248
|
+
if (options.limit) query.limit = options.limit;
|
|
249
|
+
if (options.offset) query.offset = options.offset;
|
|
250
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
251
|
+
formatOutput({ columns, headers, rows: data.data || data }, { format: program.opts().output, emptyMessage: 'No policy history found.' });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── governance template ─────────────────────────────────────────────────────
|
|
255
|
+
// Template packs: pre-built bundles of policies that can be previewed for
|
|
256
|
+
// conflicts and enabled onto an org in one shot. GOV-7 (server.js ~12375).
|
|
257
|
+
|
|
258
|
+
const template = governance
|
|
259
|
+
.command('template')
|
|
260
|
+
.description('Governance policy template packs');
|
|
261
|
+
|
|
262
|
+
// GET /api/v1/policies/templates
|
|
263
|
+
template
|
|
264
|
+
.command('list')
|
|
265
|
+
.description('List available template packs')
|
|
266
|
+
.action(async () => {
|
|
267
|
+
auth.requireAuth();
|
|
268
|
+
const ep = resolveEndpoint('v1.policies.templates.list');
|
|
269
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
270
|
+
formatOutput({
|
|
271
|
+
columns: ['id', 'name', 'description', 'policyCount'],
|
|
272
|
+
headers: ['ID', 'Name', 'Description', 'Policies'],
|
|
273
|
+
rows: data.data || data,
|
|
274
|
+
}, { format: program.opts().output, emptyMessage: 'No template packs found.' });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// GET /api/v1/policies/templates/:packId
|
|
278
|
+
template
|
|
279
|
+
.command('show')
|
|
280
|
+
.description('Show a template pack with its full policy set')
|
|
281
|
+
.argument('<packId>', 'template pack ID')
|
|
282
|
+
.action(async (packId) => {
|
|
283
|
+
auth.requireAuth();
|
|
284
|
+
const ep = resolveEndpoint('v1.policies.templates.get', { packId });
|
|
285
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
286
|
+
formatDetail(data.pack || data, { format: program.opts().output, title: `Template Pack: ${packId}` });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// POST /api/v1/policies/templates/:packId/conflicts — non-destructive preview (WRITE, WP4.2)
|
|
290
|
+
template
|
|
291
|
+
.command('conflicts')
|
|
292
|
+
.description('Check a template pack for conflicts with existing policies before enabling')
|
|
293
|
+
.argument('<packId>', 'template pack ID')
|
|
294
|
+
.action(async (packId) => {
|
|
295
|
+
auth.requireAuth();
|
|
296
|
+
const orgId = resolveOrgId(auth, program);
|
|
297
|
+
const ep = resolveEndpoint('v1.policies.templates.conflicts', { packId });
|
|
298
|
+
const data = await client.post(ep.path, { service: ep.service, body: { org_id: orgId } });
|
|
299
|
+
formatOutput({
|
|
300
|
+
// fleet #392: conflicts return templateName/existingId; `reason` has no counterpart.
|
|
301
|
+
columns: ['templateName', 'existingId'],
|
|
302
|
+
headers: ['Template Policy', 'Conflicts With'],
|
|
303
|
+
rows: data.conflicts || [],
|
|
304
|
+
}, { format: program.opts().output, emptyMessage: 'No conflicts detected.' });
|
|
305
|
+
if (data.hasConflicts && program.opts().output !== 'json') {
|
|
306
|
+
printWarning(`${data.conflicts.length} conflict(s) detected — review before enabling.`);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// POST /api/v1/policies/templates/:packId/enable — WRITE, creates policies (WP4.2)
|
|
311
|
+
template
|
|
312
|
+
.command('enable')
|
|
313
|
+
.description('Enable a template pack for the active org (creates its policies)')
|
|
314
|
+
.argument('<packId>', 'template pack ID')
|
|
315
|
+
.option('--created-by <id>', 'attribute the created policies to this actor')
|
|
316
|
+
.action(async (packId, options) => {
|
|
317
|
+
auth.requireAuth();
|
|
318
|
+
const orgId = resolveOrgId(auth, program);
|
|
319
|
+
const body = { org_id: orgId };
|
|
320
|
+
if (options.createdBy) body.created_by = options.createdBy;
|
|
321
|
+
const ep = resolveEndpoint('v1.policies.templates.enable', { packId });
|
|
322
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
323
|
+
printSuccess(`Template pack "${packId}" enabled.`);
|
|
324
|
+
formatDetail(data, { format: program.opts().output });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ── governance approval ─────────────────────────────────────────────────────
|
|
328
|
+
// Second-approver (4-eyes) sign-off on staged policy changes.
|
|
329
|
+
|
|
330
|
+
const approval = governance
|
|
331
|
+
.command('approval')
|
|
332
|
+
.description('Second-approver sign-off on staged policy changes');
|
|
333
|
+
|
|
334
|
+
// POST /api/v1/policies/approvals/:entryId/resolve — WRITE, WP4.2
|
|
335
|
+
approval
|
|
336
|
+
.command('resolve')
|
|
337
|
+
.description('Approve or reject a pending policy change')
|
|
338
|
+
.argument('<entryId>', 'policy history entry ID')
|
|
339
|
+
.requiredOption('--outcome <outcome>', 'approved | rejected')
|
|
340
|
+
.option('--reason <text>', 'reason / comment for the decision')
|
|
341
|
+
.option('--approved-by <id>', 'approver identity (defaults to the logged-in user)')
|
|
342
|
+
.action(async (entryId, options) => {
|
|
343
|
+
auth.requireAuth();
|
|
344
|
+
if (!['approved', 'rejected'].includes(options.outcome)) {
|
|
345
|
+
throw new Error("--outcome must be 'approved' or 'rejected'");
|
|
346
|
+
}
|
|
347
|
+
const tokens = auth.getTokens ? auth.getTokens() : null;
|
|
348
|
+
const approvedBy = options.approvedBy || tokens?.email || tokens?.user_id;
|
|
349
|
+
if (!approvedBy) {
|
|
350
|
+
throw new Error('--approved-by required (no stored user identity found; run `quox login` or pass --approved-by).');
|
|
351
|
+
}
|
|
352
|
+
const body = { outcome: options.outcome, approved_by: approvedBy };
|
|
353
|
+
if (options.reason) body.reason = options.reason;
|
|
354
|
+
|
|
355
|
+
const ep = resolveEndpoint('v1.policies.approvals.resolve', { entryId });
|
|
356
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
357
|
+
printSuccess(`Approval ${entryId} ${options.outcome}.`);
|
|
358
|
+
formatDetail(data.entry || data, { format: program.opts().output });
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ── governance ward ─────────────────────────────────────────────────────────
|
|
362
|
+
// Mounted via services/ward/api-cjs.cjs mountWardRoutes(app) directly on
|
|
363
|
+
// the collector app (no org-scoped path prefix).
|
|
364
|
+
|
|
365
|
+
const ward = governance
|
|
366
|
+
.command('ward')
|
|
367
|
+
.description('WARD tamper-evident chain');
|
|
368
|
+
|
|
369
|
+
// GET /ward/stats
|
|
370
|
+
ward
|
|
371
|
+
.command('status')
|
|
372
|
+
.description('Show WARD chain + witness statistics')
|
|
373
|
+
.action(async () => {
|
|
374
|
+
auth.requireAuth();
|
|
375
|
+
const data = await client.get(resolveEndpoint('ward.stats.list').path, { service: resolveEndpoint('ward.stats.list').service });
|
|
376
|
+
formatDetail({ ...data.stats, ...data.witness }, { format: program.opts().output, title: 'WARD Status' });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// GET /ward/chains
|
|
380
|
+
ward
|
|
381
|
+
.command('chains')
|
|
382
|
+
.description('List WARD chains')
|
|
383
|
+
.action(async () => {
|
|
384
|
+
auth.requireAuth();
|
|
385
|
+
const data = await client.get(resolveEndpoint('ward.chains.list').path, { service: resolveEndpoint('ward.chains.list').service });
|
|
386
|
+
formatOutput({
|
|
387
|
+
columns: ['chain_id', 'created_at'],
|
|
388
|
+
headers: ['Chain ID', 'Created'],
|
|
389
|
+
rows: data.chains || data,
|
|
390
|
+
}, { format: program.opts().output, emptyMessage: 'No chains found.' });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// GET /ward/entries?chain=...
|
|
394
|
+
ward
|
|
395
|
+
.command('entries')
|
|
396
|
+
.description('List entries in a WARD chain')
|
|
397
|
+
.option('--chain <chainId>', 'chain ID (default: production chain)')
|
|
398
|
+
.option('--limit <n>', 'max results', '50')
|
|
399
|
+
.action(async (options) => {
|
|
400
|
+
auth.requireAuth();
|
|
401
|
+
const query = { limit: options.limit };
|
|
402
|
+
if (options.chain) query.chain = options.chain;
|
|
403
|
+
const data = await client.get(resolveEndpoint('ward.entries.list').path, { service: resolveEndpoint('ward.entries.list').service, query });
|
|
404
|
+
formatOutput({
|
|
405
|
+
// fleet #392: WARD entries carry witnessed_at, not created_at.
|
|
406
|
+
columns: ['seq', 'source_kind', 'chain_id', 'witnessed_at'],
|
|
407
|
+
headers: ['Seq', 'Source', 'Chain', 'Created'],
|
|
408
|
+
rows: data.entries || data,
|
|
409
|
+
}, { format: program.opts().output, emptyMessage: 'No entries found.' });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// POST /ward/verify — non-mutating integrity check, safe to run without --confirm
|
|
413
|
+
ward
|
|
414
|
+
.command('verify')
|
|
415
|
+
.description('Verify a WARD chain\'s integrity')
|
|
416
|
+
// #622: --chain is no longer REQUIRED. An auditor who ran `ward verify`
|
|
417
|
+
// with nothing hit "error: required option --chain" with no way to discover
|
|
418
|
+
// a chain id from here, so verify dead-ended. Now bare `verify` LISTS the
|
|
419
|
+
// discoverable chains + how to pick one, instead of erroring.
|
|
420
|
+
//
|
|
421
|
+
// A `--all` that verified every chain is still NOT kept, but the underlying
|
|
422
|
+
// hazard is now fixed (GTM-G3d, 2026-09-28): server-side verify is BOUNDED, so
|
|
423
|
+
// verifying the production chain (millions of entries) no longer pegs the
|
|
424
|
+
// collector — it returns PARTIAL/BOUNDED_LARGE_CHAIN over the head window in
|
|
425
|
+
// seconds. For full coverage of a very large chain, page with
|
|
426
|
+
// --from-seq/--up-to-seq (bounded per call). Still one explicit chain at a time.
|
|
427
|
+
.option('--chain <chainId>', 'chain ID to verify (see `quox governance ward chains`)')
|
|
428
|
+
.option('--from-seq <n>', 'verify from this seq (page a large chain; bounded per call)', (v) => parseInt(v, 10))
|
|
429
|
+
.option('--up-to-seq <n>', 'verify up to this seq (page a large chain; bounded per call)', (v) => parseInt(v, 10))
|
|
430
|
+
// #622: prove ONE action end-to-end. An auditor asked "prove this agent
|
|
431
|
+
// action is in your tamper-evident log" had no CLI path; now they take the
|
|
432
|
+
// envelope id from `quox aee get <ENV_...>` and verify its receipt. Bounded
|
|
433
|
+
// (entry → covering signed tip), so it is safe on the production chain that
|
|
434
|
+
// a full --chain verify is not. --source/--source-kind generalise it to
|
|
435
|
+
// AOCL/VOLT/EXTERNAL source events.
|
|
436
|
+
.option('--envelope <envelopeId>', 'verify one AEE envelope\'s inclusion receipt (alias for --source <id> --source-kind AEE)')
|
|
437
|
+
.option('--source <sourceId>', 'verify one source event\'s inclusion receipt')
|
|
438
|
+
.option('--source-kind <kind>', 'source kind for --source (AEE|AOCL|VOLT|WARD|EXTERNAL)', 'AEE')
|
|
439
|
+
.action(async (options) => {
|
|
440
|
+
auth.requireAuth();
|
|
441
|
+
const isJson = program.opts().output === 'json';
|
|
442
|
+
|
|
443
|
+
// #622: single-action receipt path — takes precedence over chain verify.
|
|
444
|
+
const sourceId = options.envelope || options.source;
|
|
445
|
+
if (sourceId) {
|
|
446
|
+
const sourceKind = options.envelope ? 'AEE' : options.sourceKind;
|
|
447
|
+
const data = await client.post(resolveEndpoint('ward.verify_receipt').path, {
|
|
448
|
+
service: resolveEndpoint('ward.verify_receipt').service,
|
|
449
|
+
body: { source_id: sourceId, source_kind: sourceKind, chain_id: options.chain || undefined },
|
|
450
|
+
});
|
|
451
|
+
const result = data.result || data;
|
|
452
|
+
formatDetail(result, { format: program.opts().output, title: `WARD Receipt: ${sourceKind} ${sourceId}` });
|
|
453
|
+
if (result.status !== 'WITNESSED' && !isJson) {
|
|
454
|
+
printWarning(`Receipt status: ${result.status}${result.reason ? ` (${result.reason})` : ''}`);
|
|
455
|
+
}
|
|
456
|
+
if (result.status !== 'WITNESSED') process.exitCode = 1;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!options.chain) {
|
|
461
|
+
// Kill the dead-end: instead of erroring, show what CAN be verified.
|
|
462
|
+
const chainsData = await client.get(resolveEndpoint('ward.chains.list').path, { service: resolveEndpoint('ward.chains.list').service });
|
|
463
|
+
const chains = chainsData.chains || chainsData || [];
|
|
464
|
+
if (isJson) { console.log(JSON.stringify({ error: 'chain_required', hint: 'pass --chain <id>', chains }, null, 2)); process.exitCode = 1; return; }
|
|
465
|
+
printWarning('Specify --chain <id> to verify a chain. Discoverable chains:');
|
|
466
|
+
formatOutput({
|
|
467
|
+
columns: ['chain_id', 'created_at'],
|
|
468
|
+
headers: ['Chain ID', 'Created'],
|
|
469
|
+
rows: chains,
|
|
470
|
+
}, { format: program.opts().output, emptyMessage: 'No chains found.' });
|
|
471
|
+
process.exitCode = 1;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const verifyBody = { chain_id: options.chain };
|
|
476
|
+
if (Number.isFinite(options.fromSeq)) verifyBody.from_seq = options.fromSeq;
|
|
477
|
+
if (Number.isFinite(options.upToSeq)) verifyBody.up_to_seq = options.upToSeq;
|
|
478
|
+
const data = await client.post(resolveEndpoint('ward.verify').path, {
|
|
479
|
+
service: resolveEndpoint('ward.verify').service,
|
|
480
|
+
body: verifyBody,
|
|
481
|
+
});
|
|
482
|
+
const result = data.result || data;
|
|
483
|
+
formatDetail(result, { format: program.opts().output, title: `WARD Verify: ${options.chain}` });
|
|
484
|
+
// G3d: a bounded verify of a large chain returns PARTIAL/BOUNDED_LARGE_CHAIN
|
|
485
|
+
// (or PARTIAL/WINDOWED when paging) — that is a HEALTHY result, not a failure.
|
|
486
|
+
// Treat those as success (exit 0); only a genuine problem (BROKEN, or PARTIAL
|
|
487
|
+
// for INCOMPLETE_CHAIN / WINDOW_TOO_LARGE / a real gap) is exit 1.
|
|
488
|
+
const boundedOk = result.status === 'PARTIAL'
|
|
489
|
+
&& (result.reason === 'BOUNDED_LARGE_CHAIN' || result.reason === 'WINDOWED');
|
|
490
|
+
const healthy = result.status === 'INTACT' || boundedOk;
|
|
491
|
+
if (result.status && !healthy && !isJson) {
|
|
492
|
+
printWarning(`Chain status: ${result.status}${result.reason ? ` (${result.reason})` : ''}`);
|
|
493
|
+
} else if (boundedOk && !isJson) {
|
|
494
|
+
printWarning(`Chain status: verified bounded (${result.verified_range ? `seq ${result.verified_range.from}..${result.verified_range.to} of ${result.entry_count}` : 'head window'}); page with --from-seq/--up-to-seq for full coverage.`);
|
|
495
|
+
}
|
|
496
|
+
if (result.status && !healthy) process.exitCode = 1;
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// ── governance evidence ──────────────────────────────────────────────────────
|
|
500
|
+
// GET /quoxflow/evidence — collector proxy to quoxflow GET /api/evidence
|
|
501
|
+
|
|
502
|
+
governance
|
|
503
|
+
.command('evidence')
|
|
504
|
+
.description('Query VOLT/quoxflow evidence records')
|
|
505
|
+
.option('--corr <id>', 'correlation ID')
|
|
506
|
+
.option('--envelope <id>', 'envelope ID')
|
|
507
|
+
.option('--workflow <id>', 'workflow ID')
|
|
508
|
+
.option('--execution <id>', 'execution ID')
|
|
509
|
+
.option('--type <type>', 'evidence type')
|
|
510
|
+
.option('--limit <n>', 'max results')
|
|
511
|
+
.option('--offset <n>', 'result offset')
|
|
512
|
+
.action(async (options) => {
|
|
513
|
+
auth.requireAuth();
|
|
514
|
+
const query = {};
|
|
515
|
+
if (options.corr) query.corr = options.corr;
|
|
516
|
+
if (options.envelope) query.envelopeId = options.envelope;
|
|
517
|
+
if (options.workflow) query.workflowId = options.workflow;
|
|
518
|
+
if (options.execution) query.executionId = options.execution;
|
|
519
|
+
if (options.type) query.type = options.type;
|
|
520
|
+
if (options.limit) query.limit = options.limit;
|
|
521
|
+
if (options.offset) query.offset = options.offset;
|
|
522
|
+
|
|
523
|
+
const data = await client.get(resolveEndpoint('quoxflow.evidence.list').path, { service: resolveEndpoint('quoxflow.evidence.list').service, query });
|
|
524
|
+
formatOutput({
|
|
525
|
+
// fleet #392: quoxflow evidence is camelCase (createdAt).
|
|
526
|
+
columns: ['id', 'type', 'correlationId', 'workflowId', 'createdAt'],
|
|
527
|
+
headers: ['ID', 'Type', 'Correlation', 'Workflow', 'Created'],
|
|
528
|
+
rows: data.evidence || data.data || data,
|
|
529
|
+
}, { format: program.opts().output, emptyMessage: 'No evidence records found.' });
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ── governance traces ─────────────────────────────────────────────────────────
|
|
533
|
+
// GET /quoxflow/observations — collector proxy to quoxflow GET /api/observations
|
|
534
|
+
|
|
535
|
+
governance
|
|
536
|
+
.command('traces')
|
|
537
|
+
.description('Query quoxflow execution traces (observations)')
|
|
538
|
+
.option('--task <id>', 'task ID')
|
|
539
|
+
.option('--correlation <id>', 'correlation ID')
|
|
540
|
+
.option('--workflow <id>', 'workflow ID')
|
|
541
|
+
.option('--org <id>', 'org ID (overrides active org)')
|
|
542
|
+
.action(async (options) => {
|
|
543
|
+
auth.requireAuth();
|
|
544
|
+
const query = {};
|
|
545
|
+
if (options.task) query.taskId = options.task;
|
|
546
|
+
if (options.correlation) query.correlationId = options.correlation;
|
|
547
|
+
if (options.workflow) query.workflowId = options.workflow;
|
|
548
|
+
query.orgId = options.org || resolveOrgId(auth, program);
|
|
549
|
+
|
|
550
|
+
const data = await client.get(resolveEndpoint('quoxflow.observations.list').path, { service: resolveEndpoint('quoxflow.observations.list').service, query });
|
|
551
|
+
formatOutput({
|
|
552
|
+
// fleet #392: observations carry ts, not created_at.
|
|
553
|
+
columns: ['id', 'taskId', 'correlationId', 'ts'],
|
|
554
|
+
headers: ['ID', 'Task', 'Correlation', 'Created'],
|
|
555
|
+
rows: data.observations || data.data || data,
|
|
556
|
+
}, { format: program.opts().output, emptyMessage: 'No traces found.' });
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// ── governance certify ───────────────────────────────────────────────────────
|
|
560
|
+
// POST /volt/certify — the one write. Requires --confirm.
|
|
561
|
+
|
|
562
|
+
governance
|
|
563
|
+
.command('certify')
|
|
564
|
+
.description('Run VOLT certification against a run (WRITE — creates a certification record)')
|
|
565
|
+
.argument('<runId>', 'VOLT run ID')
|
|
566
|
+
.option('--retention-days <n>', 'retention period in days')
|
|
567
|
+
.option('--audit-info <json>', 'audit info as JSON')
|
|
568
|
+
.option('--compliance-info <json>', 'compliance info as JSON')
|
|
569
|
+
.option('--confirm', 'confirm you want to run certification')
|
|
570
|
+
.action(async (runId, options) => {
|
|
571
|
+
auth.requireAuth();
|
|
572
|
+
if (!options.confirm) {
|
|
573
|
+
throw new Error('Certification creates a permanent record. Add --confirm to proceed.');
|
|
574
|
+
}
|
|
575
|
+
const body = { run_id: runId };
|
|
576
|
+
if (options.retentionDays) body.retention_days = parseInt(options.retentionDays, 10);
|
|
577
|
+
if (options.auditInfo) body.audit_info = parseJSON(options.auditInfo, '--audit-info');
|
|
578
|
+
if (options.complianceInfo) body.compliance_info = parseJSON(options.complianceInfo, '--compliance-info');
|
|
579
|
+
|
|
580
|
+
const data = await client.post(resolveEndpoint('volt.certify').path, { service: resolveEndpoint('volt.certify').service, body });
|
|
581
|
+
printSuccess(`Certification created: ${data.cert_id} (tier: ${data.tier_name || data.tier})`);
|
|
582
|
+
formatDetail(data, { format: program.opts().output });
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// ── governance cert ──────────────────────────────────────────────────────────
|
|
586
|
+
// GET /volt/cert/:certId, /volt/cert/run/:runId, /volt/certs, /volt/certs/stats
|
|
587
|
+
|
|
588
|
+
const cert = governance
|
|
589
|
+
.command('cert')
|
|
590
|
+
.description('Inspect VOLT certifications');
|
|
591
|
+
|
|
592
|
+
cert
|
|
593
|
+
.command('get')
|
|
594
|
+
.description('Get a certification by ID')
|
|
595
|
+
.argument('<certId>', 'certification ID')
|
|
596
|
+
.action(async (certId) => {
|
|
597
|
+
auth.requireAuth();
|
|
598
|
+
const data = await client.get(resolveEndpoint('volt.cert.get', { certId }).path, { service: resolveEndpoint('volt.cert.get', { certId }).service });
|
|
599
|
+
formatDetail(data.cert || data, { format: program.opts().output, title: `Certification: ${certId}` });
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
cert
|
|
603
|
+
.command('for-run')
|
|
604
|
+
.description('Get the latest certification for a run')
|
|
605
|
+
.argument('<runId>', 'VOLT run ID')
|
|
606
|
+
.action(async (runId) => {
|
|
607
|
+
auth.requireAuth();
|
|
608
|
+
const data = await client.get(resolveEndpoint('volt.cert.run.get', { runId }).path, { service: resolveEndpoint('volt.cert.run.get', { runId }).service });
|
|
609
|
+
formatDetail(data.cert || data, { format: program.opts().output, title: `Certification for run: ${runId}` });
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
cert
|
|
613
|
+
.command('list')
|
|
614
|
+
.description('List certifications')
|
|
615
|
+
.option('--limit <n>', 'max results', '30')
|
|
616
|
+
.option('--offset <n>', 'result offset', '0')
|
|
617
|
+
.action(async (options) => {
|
|
618
|
+
auth.requireAuth();
|
|
619
|
+
const data = await client.get(resolveEndpoint('volt.certs.list').path, {
|
|
620
|
+
service: resolveEndpoint('volt.certs.list').service,
|
|
621
|
+
query: { limit: options.limit, offset: options.offset },
|
|
622
|
+
});
|
|
623
|
+
formatOutput({
|
|
624
|
+
columns: ['cert_id', 'run_id', 'tier', 'tier_name', 'certified_at', 'expires_at'],
|
|
625
|
+
headers: ['Cert ID', 'Run', 'Tier', 'Tier Name', 'Certified', 'Expires'],
|
|
626
|
+
rows: data.certs || data,
|
|
627
|
+
}, { format: program.opts().output, emptyMessage: 'No certifications found.' });
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
cert
|
|
631
|
+
.command('stats')
|
|
632
|
+
.description('Certification statistics')
|
|
633
|
+
.action(async () => {
|
|
634
|
+
auth.requireAuth();
|
|
635
|
+
const data = await client.get(resolveEndpoint('volt.certs.stats.list').path, { service: resolveEndpoint('volt.certs.stats.list').service });
|
|
636
|
+
formatDetail(data, { format: program.opts().output, title: 'Certification Stats' });
|
|
637
|
+
});
|
|
638
|
+
}
|