quoxcli 0.2.0 → 0.4.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 +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +46 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +14 -0
- package/lib/api/endpoints/contactsq.js +6 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +44 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +26 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +179 -0
- package/lib/api/endpoints/objectives.js +12 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +12 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +120 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +131 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +170 -27
- package/lib/commands/agentAttach.js +140 -13
- package/lib/commands/approvals.js +78 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +146 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +180 -26
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +35 -38
- package/lib/commands/contacts.js +134 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +107 -6
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +336 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +254 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +52 -155
- package/lib/commands/loop.js +291 -0
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +59 -1
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +30 -3
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +217 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +69 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/research.js +79 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +367 -0
- package/lib/commands/run.js +82 -10
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +405 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +68 -9
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actingAs.js +2 -2
- package/lib/core/actionItems.js +115 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +152 -35
- package/lib/core/commandRegistrars.js +130 -0
- package/lib/core/config.js +143 -25
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +105 -5
- package/lib/core/governedSession.js +1 -3
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +533 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +11 -11
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/shortcut.js +83 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +366 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/EXTENSIONS.md +23 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +690 -25
- package/lib/quoxon/app.js +3586 -250
- package/lib/quoxon/approvalFacts.js +96 -0
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +384 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +124 -0
- package/lib/quoxon/commands.js +429 -39
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +20 -0
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +46 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +192 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +151 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +266 -26
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/inboxView.js +79 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +51 -8
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +101 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +222 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +46 -7
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +59 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +12 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +62 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +75 -37
- package/lib/quoxon/render.js +740 -124
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +117 -17
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +954 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +182 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +94 -0
- package/lib/quoxon/slash/cliLeaves.json +1023 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +22 -8
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +95 -13
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +90 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +134 -0
- package/lib/quoxon/windowOrg.js +123 -0
- package/lib/server/devServers.js +137 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +5 -1
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
- package/lib/core/websiteSession.js +0 -148
package/lib/commands/fleet.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { missingReasonWarning } from '../core/callerSource.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import tls from 'node:tls';
|
|
8
|
+
import { X509Certificate, createHash } from 'node:crypto';
|
|
7
9
|
import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
8
10
|
import { events, printEvent, line as glyphLine } from '../core/quoxglyph.js';
|
|
9
11
|
import { parseJSON } from './helpers.js';
|
|
@@ -211,10 +213,166 @@ export function parseSentinel(output) {
|
|
|
211
213
|
/** The full install pipe, using install.sh's REAL flags. */
|
|
212
214
|
export function buildRehomeInstallCommand({ collector, hostId, orgId }) {
|
|
213
215
|
const base = String(collector).replace(/\/+$/, '');
|
|
214
|
-
|
|
216
|
+
// quox#1223: fail loud, no `curl | bash` (see buildEnrolCommand). TLS verification stays on.
|
|
217
|
+
return `( f=$(mktemp) && trap 'rm -f "$f"' EXIT && curl -fsSL ${base}/install -o "$f" && sudo bash "$f"`
|
|
215
218
|
+ ` ${INSTALL_SH_FLAGS.hostId} ${shellSingleQuote(hostId)}`
|
|
216
219
|
+ ` ${INSTALL_SH_FLAGS.collector} ${shellSingleQuote(base)}`
|
|
217
|
-
+ ` ${INSTALL_SH_FLAGS.org} ${shellSingleQuote(orgId)}`;
|
|
220
|
+
+ ` ${INSTALL_SH_FLAGS.org} ${shellSingleQuote(orgId)} )`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── fleet enrol (Z2H-AGENTINST) ─────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
export const ENROL_OS = ['linux', 'darwin'];
|
|
226
|
+
export const ENROL_ARCH = ['amd64', 'arm64'];
|
|
227
|
+
|
|
228
|
+
/** Exact wording when GET /install answers 503 (no installer shipped). */
|
|
229
|
+
export const ENROL_NO_INSTALLER_MESSAGE =
|
|
230
|
+
'this install has no agent installer yet (quoxagent binaries not shipped); enrolment is not available';
|
|
231
|
+
|
|
232
|
+
/** Argument validation for `fleet enrol`. Returns a list of errors. */
|
|
233
|
+
export function validateEnrolArgs(hostId, options = {}) {
|
|
234
|
+
const usage = 'Usage: quox fleet enrol <host> [--os linux --arch amd64|arm64] [--run]';
|
|
235
|
+
const errors = [];
|
|
236
|
+
if (!hostId || !String(hostId).trim()) errors.push(`Missing <host>. ${usage}`);
|
|
237
|
+
else if (/[\s'"`$\;|&<>]/.test(String(hostId))) {
|
|
238
|
+
errors.push(`<host> "${hostId}" has characters that are not valid in a host id (spaces, quotes, shell symbols). ${usage}`);
|
|
239
|
+
}
|
|
240
|
+
if (options.os !== undefined && !ENROL_OS.includes(String(options.os))) {
|
|
241
|
+
errors.push(`--os must be one of ${ENROL_OS.join(', ')} (got "${options.os}"). ${usage}`);
|
|
242
|
+
}
|
|
243
|
+
if (options.arch !== undefined && !ENROL_ARCH.includes(String(options.arch))) {
|
|
244
|
+
errors.push(`--arch must be one of ${ENROL_ARCH.join(', ')} (got "${options.arch}"). ${usage}`);
|
|
245
|
+
}
|
|
246
|
+
return errors;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Pull the collector URL the serving collector templated into install.sh's
|
|
251
|
+
* `COLLECTOR_URL="${COLLECTOR_URL:-<url>}"` default arm. Empty string when the
|
|
252
|
+
* collector has no event URL configured (the script then refuses to run
|
|
253
|
+
* without --collector).
|
|
254
|
+
*/
|
|
255
|
+
export function parseInstallerCollectorUrl(script) {
|
|
256
|
+
const m = /^COLLECTOR_URL="\$\{COLLECTOR_URL:-([^}"]*)\}"$/m.exec(String(script || ''));
|
|
257
|
+
return m ? m[1].trim() : '';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Probe GET <base>/install. Returns { collectorUrl, bytes } or throws an Error
|
|
262
|
+
* whose message is plain English, never a raw status dump. `fetchImpl` is
|
|
263
|
+
* injectable for tests.
|
|
264
|
+
*/
|
|
265
|
+
export async function probeEnrolInstaller(base, fetchImpl = fetch) {
|
|
266
|
+
const root = String(base).replace(/\/+$/, '');
|
|
267
|
+
const url = `${root}/install`;
|
|
268
|
+
let resp;
|
|
269
|
+
try {
|
|
270
|
+
resp = await fetchImpl(url, { signal: AbortSignal.timeout(10000) });
|
|
271
|
+
} catch (err) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Could not reach ${url} (${err.message}). Check the Quox instance is running and reachable from this machine.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (resp.status === 503) throw new Error(`Enrolment is not available: ${ENROL_NO_INSTALLER_MESSAGE}.`);
|
|
277
|
+
if (!resp.ok) {
|
|
278
|
+
throw new Error(`Enrolment is not available: GET ${url} answered HTTP ${resp.status}, not an installer.`);
|
|
279
|
+
}
|
|
280
|
+
const script = await resp.text();
|
|
281
|
+
if (!script.startsWith(INSTALL_SCRIPT_MAGIC)) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Enrolment is not available: ${url} answered but not with the QuoxAgent installer `
|
|
284
|
+
+ '(a proxy error page or a login redirect is in the way). Point the profile at the Quox instance itself.'
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return { collectorUrl: parseInstallerCollectorUrl(script), bytes: script.length };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The exact command to run ON the host. `base` is the URL the installer is
|
|
292
|
+
* fetched from (the collector's own templated LAN URL when it has one, else
|
|
293
|
+
* the instance URL); `collector` is the URL the agent will report to.
|
|
294
|
+
*
|
|
295
|
+
* Fail loud (quox#1223): the script is downloaded to a temp file with
|
|
296
|
+
* `curl -f` and only then run, inside a subshell, so a curl failure (TLS
|
|
297
|
+
* error, HTTP error) stops the command with curl's non-zero exit and nothing
|
|
298
|
+
* is piped into bash. TLS verification stays ON unless the operator passes
|
|
299
|
+
* `insecureTls` (the explicit --insecure-tls opt-in).
|
|
300
|
+
*/
|
|
301
|
+
export function buildEnrolCommand({ base, collector, hostId, insecureTls = false, tlsPin = null, binarySha256 = null }) {
|
|
302
|
+
const root = String(base).replace(/\/+$/, '');
|
|
303
|
+
const coll = String(collector || root).replace(/\/+$/, '');
|
|
304
|
+
// REL-423: binary integrity out of band. install.sh fetches the agent binary from the collector URL (plain http
|
|
305
|
+
// on a default install) and fails closed when BINARY_SHA256 does not match; the CLI read this hash over its own
|
|
306
|
+
// verified connection, so a MITM on the binary download cannot swap it.
|
|
307
|
+
const sha = /^[0-9a-f]{64}$/.test(String(binarySha256 || '')) ? `env BINARY_SHA256=${binarySha256} ` : '';
|
|
308
|
+
const flags = ` ${INSTALL_SH_FLAGS.hostId} ${shellSingleQuote(hostId)} ${INSTALL_SH_FLAGS.collector} ${shellSingleQuote(coll)} )`;
|
|
309
|
+
if (tlsPin && !insecureTls) {
|
|
310
|
+
// REL-423: a self-signed edge with TLS verification ON and no -k. The install's own certificate rides in the
|
|
311
|
+
// command as the only trusted CA (--cacert: chain + hostname checked) and its public key is pinned too
|
|
312
|
+
// (--pinnedpubkey: a different key aborts with curl exit 90). Both were read over a verified handshake.
|
|
313
|
+
const { pem, pin } = pemAndPinParts(tlsPin);
|
|
314
|
+
return `( f=$(mktemp) && c=$(mktemp) && trap 'rm -f "$f" "$c"' EXIT && printf -- '${pem}' > "$c"`
|
|
315
|
+
+ ` && curl -fsSL --cacert "$c" --pinnedpubkey 'sha256//${pin}' ${root}/install -o "$f" && sudo ${sha}bash "$f"` + flags;
|
|
316
|
+
}
|
|
317
|
+
const curl = insecureTls ? 'curl -fsSLk' : 'curl -fsSL';
|
|
318
|
+
if (sha) return `( f=$(mktemp) && trap 'rm -f "$f"' EXIT && ${curl} ${root}/install -o "$f" && sudo ${sha}bash "$f"` + flags;
|
|
319
|
+
return `( f=$(mktemp) && trap 'rm -f "$f"' EXIT && ${curl} ${root}/install -o "$f" && sudo bash "$f"`
|
|
320
|
+
+ ` ${INSTALL_SH_FLAGS.hostId} ${shellSingleQuote(hostId)}`
|
|
321
|
+
+ ` ${INSTALL_SH_FLAGS.collector} ${shellSingleQuote(coll)} )`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Validate a { pem, pin } pair and return the shell-safe parts (base64 only, so nothing can break out of quotes). */
|
|
325
|
+
function pemAndPinParts({ pem, pin }) {
|
|
326
|
+
const body = String(pem || '').replace(/-----(BEGIN|END) CERTIFICATE-----/g, '').replace(/\s+/g, '');
|
|
327
|
+
if (!/^[A-Za-z0-9+/]+=*$/.test(body) || !/^[A-Za-z0-9+/]{43}=$/.test(String(pin || ''))) throw new Error('invalid TLS pin');
|
|
328
|
+
const lines = body.match(/.{1,64}/g).join('\\n');
|
|
329
|
+
return { pem: `-----BEGIN CERTIFICATE-----\\n${lines}\\n-----END CERTIFICATE-----\\n`, pin };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* REL-423: the URL another host fetches the installer from. The instance's own TLS edge (it proxies /install and
|
|
336
|
+
* /download/quoxagent-* to the collector), with a loopback profile host (the host CLI's `local` profile is
|
|
337
|
+
* https://localhost:3000) swapped for the LAN host the collector advertises (COLLECTOR_LAN_URL, set by setup.sh).
|
|
338
|
+
* Returns null when a loopback base has no LAN host to swap in.
|
|
339
|
+
*/
|
|
340
|
+
export function edgeInstallerBase(base, collectorUrl) {
|
|
341
|
+
const u = new URL(String(base));
|
|
342
|
+
if (LOOPBACK_HOSTNAMES.has(u.hostname)) {
|
|
343
|
+
let lan = '';
|
|
344
|
+
try { lan = collectorUrl ? new URL(collectorUrl).hostname : ''; } catch { lan = ''; }
|
|
345
|
+
if (!lan || LOOPBACK_HOSTNAMES.has(lan)) return null;
|
|
346
|
+
u.hostname = lan;
|
|
347
|
+
}
|
|
348
|
+
return `${u.protocol}//${u.host}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* REL-423: read the edge's certificate over a VERIFIED handshake (Node's CAs plus NODE_EXTRA_CA_CERTS, hostname
|
|
353
|
+
* checked) and return { pem, pin } (pin = base64 SHA-256 of the SPKI, curl's --pinnedpubkey form). Throws with a
|
|
354
|
+
* plain reason when the certificate cannot be verified, so no pin is ever taken from an unverified peer.
|
|
355
|
+
*/
|
|
356
|
+
export function readEdgeTlsPin(url, { connect = tls.connect, timeoutMs = 10000 } = {}) {
|
|
357
|
+
const u = new URL(String(url));
|
|
358
|
+
const host = u.hostname.replace(/^\[|\]$/g, '');
|
|
359
|
+
const port = Number(u.port || 443);
|
|
360
|
+
const isIp = /^[0-9.]+$/.test(host) || host.includes(':');
|
|
361
|
+
return new Promise((resolve, reject) => {
|
|
362
|
+
const sock = connect({ host, port, servername: isIp ? undefined : host, rejectUnauthorized: true });
|
|
363
|
+
const done = (fn, v) => { try { sock.destroy(); } catch { /* closed */ } fn(v); };
|
|
364
|
+
sock.setTimeout?.(timeoutMs, () => done(reject, new Error(`timed out reading the TLS certificate of ${u.host}`)));
|
|
365
|
+
sock.once('error', (err) => done(reject, new Error(`cannot verify the TLS certificate of ${u.host} (${err.code || err.message})`)));
|
|
366
|
+
sock.once('secureConnect', () => {
|
|
367
|
+
try {
|
|
368
|
+
const raw = sock.getPeerCertificate(true)?.raw;
|
|
369
|
+
if (!raw) throw new Error(`no certificate from ${u.host}`);
|
|
370
|
+
const x = new X509Certificate(raw);
|
|
371
|
+
const spki = x.publicKey.export({ type: 'spki', format: 'der' });
|
|
372
|
+
done(resolve, { pem: x.toString(), pin: createHash('sha256').update(spki).digest('base64') });
|
|
373
|
+
} catch (err) { done(reject, err); }
|
|
374
|
+
});
|
|
375
|
+
});
|
|
218
376
|
}
|
|
219
377
|
|
|
220
378
|
/** Argument validation for `fleet codex-provision`. Returns a list of errors. */
|
|
@@ -407,7 +565,20 @@ export function registerFleetCommands(program, ctx) {
|
|
|
407
565
|
.action(async () => {
|
|
408
566
|
auth.requireAuth();
|
|
409
567
|
const ep = resolveEndpoint('v1.fleet.hosts.list');
|
|
410
|
-
|
|
568
|
+
// quox#1224: on an install with no Bastion the collector answers from the agent registry (200) or, with
|
|
569
|
+
// no agents either, a 503 whose body says so in a sentence. Either way the operator reads that sentence,
|
|
570
|
+
// never a bare status code.
|
|
571
|
+
let data;
|
|
572
|
+
try {
|
|
573
|
+
data = await client.get(ep.path, { service: ep.service });
|
|
574
|
+
} catch (err) {
|
|
575
|
+
if (err && err.status === 503 && err.body && err.body.bastion === false) {
|
|
576
|
+
data = { hosts: [], bastion: false, message: err.body.message || err.body.error };
|
|
577
|
+
} else {
|
|
578
|
+
throw err;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (data && data.bastion === false && data.message && program.opts().output !== 'json') printInfo(data.message);
|
|
411
582
|
const raw = data.hosts || data || [];
|
|
412
583
|
const rows = (raw || []).map(h => ({
|
|
413
584
|
id: h.id,
|
|
@@ -421,7 +592,32 @@ export function registerFleetCommands(program, ctx) {
|
|
|
421
592
|
columns: ['id', 'ip', 'group', 'state', 'health', 'last_seen'],
|
|
422
593
|
headers: ['Host', 'IP', 'Group', 'State', 'Health', 'Last Seen'],
|
|
423
594
|
rows,
|
|
424
|
-
}, { format: program.opts().output, emptyMessage: 'No hosts in the Bastion registry.' });
|
|
595
|
+
}, { format: program.opts().output, emptyMessage: data && data.bastion === false ? 'No agents enrolled yet.' : 'No hosts in the Bastion registry.' });
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
fleet
|
|
599
|
+
.command('ping [host]')
|
|
600
|
+
.description('Refresh Bastion host state by pinging every host (or one); org admin, writes a quox.fleet.ping evidence envelope')
|
|
601
|
+
.action(async (host) => {
|
|
602
|
+
auth.requireAuth();
|
|
603
|
+
const ep = resolveEndpoint('v1.fleet.ping');
|
|
604
|
+
const data = await client.post(ep.path, { service: ep.service, body: host ? { host } : {}, timeout: 60000 });
|
|
605
|
+
const results = data.results || [];
|
|
606
|
+
const summary = data.summary || { requested: results.length, reachable: results.filter(r => r.reachable).length, unreachable: results.filter(r => !r.reachable).length };
|
|
607
|
+
if (program.opts().output === 'json') {
|
|
608
|
+
formatDetail({ success: data.success !== false, results, summary }, { format: 'json' });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
formatOutput({
|
|
612
|
+
columns: ['host_id', 'reachable', 'latency'],
|
|
613
|
+
headers: ['Host', 'Reachable', 'Latency'],
|
|
614
|
+
rows: results.map(r => ({
|
|
615
|
+
host_id: r.host_id,
|
|
616
|
+
reachable: r.reachable ? 'yes' : (r.error ? `no (${r.error})` : 'no'),
|
|
617
|
+
latency: typeof r.latency_ms === 'number' ? `${r.latency_ms}ms` : '-',
|
|
618
|
+
})),
|
|
619
|
+
}, { format: program.opts().output, emptyMessage: 'No hosts answered the ping.' });
|
|
620
|
+
printInfo(`${summary.reachable} of ${summary.requested} reachable, ${summary.unreachable} unreachable`);
|
|
425
621
|
});
|
|
426
622
|
|
|
427
623
|
fleet
|
|
@@ -514,6 +710,10 @@ export function registerFleetCommands(program, ctx) {
|
|
|
514
710
|
// quox#791: a held call prints how to read the output once approved.
|
|
515
711
|
const heldId = data && data.pending && (data.approvalId || (data._approval && data._approval.id));
|
|
516
712
|
if (heldId) console.error(`after approval run: quox approvals result ${heldId}`);
|
|
713
|
+
// quox#999: a failed tool call must exit non-zero, or a script/CI step reads success on
|
|
714
|
+
// failure. A HELD (pending) call is not a failure — it awaits a human — so it stays 0.
|
|
715
|
+
const failed = (data && data.success === false) || (data && data.result && data.result.success === false);
|
|
716
|
+
if (failed && !(data && data.pending)) process.exitCode = 1;
|
|
517
717
|
});
|
|
518
718
|
|
|
519
719
|
fleet
|
|
@@ -844,6 +1044,138 @@ export function registerFleetCommands(program, ctx) {
|
|
|
844
1044
|
console.log();
|
|
845
1045
|
});
|
|
846
1046
|
|
|
1047
|
+
fleet
|
|
1048
|
+
.command('enrol <host>')
|
|
1049
|
+
.alias('enroll')
|
|
1050
|
+
.description('Print (default; or with --run, execute over the governed path) the exact command that enrols a host into this Quox instance. Needs the instance to ship the quoxagent installer.')
|
|
1051
|
+
.option('--os <os>', `target OS: ${ENROL_OS.join('|')} (default: linux)`)
|
|
1052
|
+
.option('--arch <arch>', `target CPU: ${ENROL_ARCH.join('|')} (default: amd64)`)
|
|
1053
|
+
// NOTE: no --print flag. Printing is the default, and `--print` anywhere in argv is claimed by the
|
|
1054
|
+
// root headless-chat router (lib/quoxon/route.js), which would swallow this whole command.
|
|
1055
|
+
.option('--run', 'run it on <host> through the governed exec path (needs <host> already reachable by the Bastion; may wait on approval)')
|
|
1056
|
+
.option('--insecure-tls', 'opt in to skipping TLS verification when fetching the installer (self-signed certs). Off by default; prints a warning')
|
|
1057
|
+
.option('--exec-org <id>', 'org ID for the governed exec calls (with --run)')
|
|
1058
|
+
.option('--wait <seconds>', 'with --run: how long to wait for the host to show up', '120')
|
|
1059
|
+
.action(async (hostId, options) => {
|
|
1060
|
+
auth.requireAuth();
|
|
1061
|
+
const errors = validateEnrolArgs(hostId, options);
|
|
1062
|
+
if (errors.length) throw new Error(errors.join('\n'));
|
|
1063
|
+
|
|
1064
|
+
const host = String(hostId).trim();
|
|
1065
|
+
const os = options.os || 'linux';
|
|
1066
|
+
const arch = options.arch || 'amd64';
|
|
1067
|
+
const isJson = program.opts().output === 'json';
|
|
1068
|
+
const say = (fn, msg) => { if (!isJson) fn(msg); };
|
|
1069
|
+
const base = String(ctx.config.getBaseUrl()).replace(/\/+$/, '');
|
|
1070
|
+
|
|
1071
|
+
// 1. Is there an installer at all? Never hand out a command that cannot work.
|
|
1072
|
+
const probe = await probeEnrolInstaller(base);
|
|
1073
|
+
|
|
1074
|
+
// 2. A binary must be staged for the platform; its hash (read over this verified connection) is pinned
|
|
1075
|
+
// into the command as BINARY_SHA256 (REL-423), so the host checks the binary it downloads.
|
|
1076
|
+
const binUrl = `${base}/download/quoxagent-${os}-${arch}.sha256`;
|
|
1077
|
+
let bin;
|
|
1078
|
+
try { bin = await fetch(binUrl, { signal: AbortSignal.timeout(10000) }); }
|
|
1079
|
+
catch (err) { throw new Error(`Could not reach ${binUrl} (${err.message}).`); }
|
|
1080
|
+
if (!bin.ok) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
`Enrolment is not available for ${os}/${arch}: this install has no quoxagent binary staged for it (HTTP ${bin.status}).`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const binarySha256 = (/^([0-9a-f]{64})\b/.exec(await bin.text()) || [])[1] || null;
|
|
1086
|
+
if (!binarySha256) throw new Error(`Enrolment is not available for ${os}/${arch}: ${binUrl} did not return a SHA-256.`);
|
|
1087
|
+
|
|
1088
|
+
const collector = probe.collectorUrl || base;
|
|
1089
|
+
const insecureTls = Boolean(options.insecureTls);
|
|
1090
|
+
// REL-423 (supersedes the quox#1223 plain-http collector fetch): an https instance hands out its own TLS edge
|
|
1091
|
+
// (/install is proxied to the collector there), with TLS verification ON for its self-signed certificate:
|
|
1092
|
+
// the certificate and its key pin are read now over a verified handshake and travel in the command.
|
|
1093
|
+
// --insecure-tls (explicit opt-in) and a plain-http instance keep the collector URL, as before.
|
|
1094
|
+
let installerBase = probe.collectorUrl || base;
|
|
1095
|
+
let tlsPin = null;
|
|
1096
|
+
if (/^https:/i.test(base) && !insecureTls) {
|
|
1097
|
+
installerBase = edgeInstallerBase(base, probe.collectorUrl);
|
|
1098
|
+
if (!installerBase) {
|
|
1099
|
+
throw new Error(
|
|
1100
|
+
`Enrolment needs this server's LAN address: the profile points at ${base} and the collector advertises no `
|
|
1101
|
+
+ 'LAN URL. Set COLLECTOR_LAN_URL=http://<this-server-LAN-address>:9848 in the install\'s .env '
|
|
1102
|
+
+ '(./scripts/setup.sh does this) and restart the collector, or use a profile with the LAN address.'
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
try { tlsPin = await readEdgeTlsPin(installerBase); }
|
|
1106
|
+
catch (err) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
`Cannot hand out a verified enrol command: ${err.message}. If the certificate does not name `
|
|
1109
|
+
+ `${new URL(installerBase).hostname}, reissue it (rm ssl/quox.crt ssl/quox.key && ./scripts/setup.sh); `
|
|
1110
|
+
+ 'or re-run with --insecure-tls to accept the risk explicitly.'
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const command = buildEnrolCommand({ base: installerBase, collector, hostId: host, insecureTls, tlsPin, binarySha256 });
|
|
1115
|
+
|
|
1116
|
+
if (!options.run) {
|
|
1117
|
+
if (isJson) {
|
|
1118
|
+
console.log(JSON.stringify({ host_id: host, os, arch, installer_url: `${installerBase}/install`, collector, insecure_tls: insecureTls, tls_pin: tlsPin ? `sha256//${tlsPin.pin}` : null, binary_sha256: binarySha256, command }, null, 2));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (!probe.collectorUrl) {
|
|
1122
|
+
printWarning(`The installer has no collector URL configured (COLLECTOR_LAN_URL / COLLECTOR_PUBLIC_URL on the collector); the command passes ${collector} explicitly.`);
|
|
1123
|
+
}
|
|
1124
|
+
if (insecureTls) {
|
|
1125
|
+
printWarning('--insecure-tls: the command skips TLS certificate verification. Anyone on the path can swap the installer, which runs as root. Use it only on a network you trust.');
|
|
1126
|
+
} else if (/^http:/i.test(installerBase)) {
|
|
1127
|
+
printWarning(`The installer is fetched over plain http (${installerBase}); the agent binary is still checked against BINARY_SHA256.`);
|
|
1128
|
+
}
|
|
1129
|
+
console.log();
|
|
1130
|
+
console.log(`Run this on ${chalk.cyan(host)} (${os}/${arch}), as a user who can sudo:`);
|
|
1131
|
+
console.log();
|
|
1132
|
+
console.log(` ${chalk.bold(command)}`);
|
|
1133
|
+
console.log();
|
|
1134
|
+
console.log(chalk.dim(' Then `quox fleet agents` and `quox fleet hosts` list it once it heartbeats.'));
|
|
1135
|
+
console.log(chalk.dim(' The host must be able to reach ' + collector + ' (the instance\'s collector).'));
|
|
1136
|
+
console.log();
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// --run: governed ssh_exec to <host>, detached and polled.
|
|
1141
|
+
say(printInfo, `Installing on ${host} from ${base} (detached, polled)...`);
|
|
1142
|
+
const install = await runDetachedStep(host, {
|
|
1143
|
+
unit: 'quoxagent-enrol',
|
|
1144
|
+
command,
|
|
1145
|
+
orgOverride: options.execOrg,
|
|
1146
|
+
});
|
|
1147
|
+
if (install.state === 'timeout') {
|
|
1148
|
+
throw new Error(`The install did not finish in time on ${host}. Check \`systemctl status quoxagent-enrol\` and ${install.log} on the host, then re-run.`);
|
|
1149
|
+
}
|
|
1150
|
+
if (install.state === 'failed') {
|
|
1151
|
+
throw new Error(`QuoxAgent install failed on ${host} (exit ${install.exitCode}):\n${install.text}`);
|
|
1152
|
+
}
|
|
1153
|
+
say(printSuccess, ' install completed.');
|
|
1154
|
+
|
|
1155
|
+
const waitSeconds = Math.max(0, Number.parseInt(options.wait, 10) || 0);
|
|
1156
|
+
const deadline = Date.now() + waitSeconds * 1000;
|
|
1157
|
+
let seen = false;
|
|
1158
|
+
let detail = 'not listed yet';
|
|
1159
|
+
do {
|
|
1160
|
+
try {
|
|
1161
|
+
const ep = resolveEndpoint('v1.agents.list');
|
|
1162
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
1163
|
+
const agents = data.agents || data || [];
|
|
1164
|
+
seen = (Array.isArray(agents) ? agents : []).some((a) => a.host_id === host);
|
|
1165
|
+
detail = seen ? 'heartbeating' : `agent list has ${Array.isArray(agents) ? agents.length : 0} entries, none matching ${host}`;
|
|
1166
|
+
} catch (err) { detail = err.message; }
|
|
1167
|
+
if (seen || Date.now() >= deadline) break;
|
|
1168
|
+
await sleep(5000);
|
|
1169
|
+
} while (Date.now() < deadline);
|
|
1170
|
+
|
|
1171
|
+
if (isJson) {
|
|
1172
|
+
console.log(JSON.stringify({ host_id: host, command, install: { state: install.state, exit_code: install.exitCode }, enrolled: seen, detail }, null, 2));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (seen) printEvent(glyphLine('bastion', 'host enrolled', { result: `${host} -> ${collector}`, status: 'ok' }));
|
|
1176
|
+
else printWarning(`${host} is not listed yet (${detail}). The install succeeded; check \`quox fleet agents\` shortly.`);
|
|
1177
|
+
});
|
|
1178
|
+
|
|
847
1179
|
fleet
|
|
848
1180
|
.command('watch')
|
|
849
1181
|
.description('Watch fleet status in real-time')
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { formatOutput, formatDetail, printSuccess, printWarning } from '../core/output.js';
|
|
23
|
-
import { resolveOrgId, parseJSON } from './helpers.js';
|
|
23
|
+
import { resolveOrgId, parseJSON, withTimeoutHint } from './helpers.js';
|
|
24
24
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
25
25
|
|
|
26
26
|
export function registerGovernanceCommands(program, ctx) {
|
|
@@ -396,17 +396,26 @@ export function registerGovernanceCommands(program, ctx) {
|
|
|
396
396
|
.description('List entries in a WARD chain')
|
|
397
397
|
.option('--chain <chainId>', 'chain ID (default: production chain)')
|
|
398
398
|
.option('--limit <n>', 'max results', '50')
|
|
399
|
+
.option('--before-seq <n>', 'only entries with seq below this (cursor for the next page)')
|
|
399
400
|
.action(async (options) => {
|
|
400
401
|
auth.requireAuth();
|
|
401
402
|
const query = { limit: options.limit };
|
|
402
403
|
if (options.chain) query.chain = options.chain;
|
|
403
|
-
|
|
404
|
+
if (options.beforeSeq) query.before_seq = options.beforeSeq;
|
|
405
|
+
const data = await withTimeoutHint(
|
|
406
|
+
'quox governance ward entries',
|
|
407
|
+
'Try a smaller --limit, or page with --before-seq <n>; pass --chain to pick a smaller chain.',
|
|
408
|
+
() => client.get(resolveEndpoint('ward.entries.list').path, { service: resolveEndpoint('ward.entries.list').service, query }),
|
|
409
|
+
);
|
|
404
410
|
formatOutput({
|
|
405
411
|
// fleet #392: WARD entries carry witnessed_at, not created_at.
|
|
406
412
|
columns: ['seq', 'source_kind', 'chain_id', 'witnessed_at'],
|
|
407
413
|
headers: ['Seq', 'Source', 'Chain', 'Created'],
|
|
408
414
|
rows: data.entries || data,
|
|
409
415
|
}, { format: program.opts().output, emptyMessage: 'No entries found.' });
|
|
416
|
+
if (data && data.has_more && data.next_before_seq != null && program.opts().output !== 'json' && program.opts().output !== 'quiet') {
|
|
417
|
+
console.error(`More entries available: --before-seq ${data.next_before_seq}`);
|
|
418
|
+
}
|
|
410
419
|
});
|
|
411
420
|
|
|
412
421
|
// POST /ward/verify — non-mutating integrity check, safe to run without --confirm
|
|
@@ -562,7 +571,7 @@ export function registerGovernanceCommands(program, ctx) {
|
|
|
562
571
|
governance
|
|
563
572
|
.command('certify')
|
|
564
573
|
.description('Run VOLT certification against a run (WRITE — creates a certification record)')
|
|
565
|
-
.argument('<runId>', 'VOLT run ID')
|
|
574
|
+
.argument('<runId>', 'VOLT run ID (VR_...) or the workflow run (execution) id `quox workflow run` printed')
|
|
566
575
|
.option('--retention-days <n>', 'retention period in days')
|
|
567
576
|
.option('--audit-info <json>', 'audit info as JSON')
|
|
568
577
|
.option('--compliance-info <json>', 'compliance info as JSON')
|
package/lib/commands/gripe.js
CHANGED
|
@@ -33,14 +33,23 @@ export function registerGripeCommands(program, { client, auth }) {
|
|
|
33
33
|
.argument('<complaint...>', 'the gripe, e.g. "sidebar tooltip clips on resize"')
|
|
34
34
|
.option('--repo <owner/name>', 'target repo (GRIPE defaults sensibly when omitted)')
|
|
35
35
|
.option('--label <label>', 'issue label, e.g. bug, idea, fleet')
|
|
36
|
+
.option('--public', 'confirm filing to a PUBLIC repo (asks y/N; refused if text has internal hostnames/paths)')
|
|
36
37
|
.option('--yes', 'skip the confirmation step and let GRIPE file it directly')
|
|
37
38
|
.action(async (complaintParts, options) => {
|
|
38
39
|
auth.requireAuth();
|
|
39
40
|
const complaint = complaintParts.join(' ').trim();
|
|
40
41
|
if (!complaint) throw new Error('say what is wrong: quox gripe "the thing that is broken"');
|
|
41
42
|
|
|
42
|
-
printEvent(glyphLine('quox', 'filing gripe via GRIPE'));
|
|
43
43
|
const orgId = resolveOrgId(auth, program);
|
|
44
|
+
if (options.public) {
|
|
45
|
+
// QUOXON-PRIVACY: public repos only via the deterministic path with a
|
|
46
|
+
// human y/N; the GRIPE agent can never confirm a public filing.
|
|
47
|
+
if (!options.repo) throw new Error('--public needs an explicit --repo <name>');
|
|
48
|
+
const r = await fileDeterministically({ client, complaint, options, orgId, confirmPublic: promptConfirmPublic });
|
|
49
|
+
if (!r.filed) process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
printEvent(glyphLine('quox', 'filing gripe via GRIPE'));
|
|
44
53
|
const data = await askGripe({ client, complaint, repo: options.repo, label: options.label, yes: !!options.yes, orgId });
|
|
45
54
|
|
|
46
55
|
if (program.opts().output === 'json') {
|
|
@@ -98,7 +107,7 @@ export async function settleGripe({ client, data, complaint, options = {}, orgId
|
|
|
98
107
|
// is what stops the outcome from depending on how the agent phrased its
|
|
99
108
|
// reply. Absent (older collector), classification falls back to prose.
|
|
100
109
|
const toolsUsed = Array.isArray(data?.tools_used) ? data.tools_used : null;
|
|
101
|
-
const outcome = classifyGripeResponse(response, { assumedYes: yes, toolsUsed });
|
|
110
|
+
const outcome = classifyGripeResponse(response, { assumedYes: yes, toolsUsed, complaint });
|
|
102
111
|
// quox#668: with --yes, a non-filing agent run is followed by the
|
|
103
112
|
// deterministic file. Printing the agent's prose first ("no tool, cannot
|
|
104
113
|
// file") right above "issue created" contradicted the real outcome. In
|
|
@@ -162,11 +171,26 @@ export async function settleGripe({ client, data, complaint, options = {}, orgId
|
|
|
162
171
|
// also cannot file. Still NOT auto-run after auth-unavailable (a 403/429/503
|
|
163
172
|
// is a platform outage the proxy would hit too; retrying shortly is the fix).
|
|
164
173
|
// Returns { filed, url?, error? } so the caller can decide the message.
|
|
165
|
-
|
|
174
|
+
async function promptConfirmPublic(repo) {
|
|
175
|
+
if (!process.stdin.isTTY) return false;
|
|
176
|
+
const { createInterface } = await import('node:readline/promises');
|
|
177
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
178
|
+
try {
|
|
179
|
+
const a = await rl.question(`${repo} is PUBLIC. This issue will be world-readable. File it? [y/N] `);
|
|
180
|
+
return /^y(es)?$/i.test(a.trim());
|
|
181
|
+
} finally { rl.close(); }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function fileDeterministically({ client, complaint, options, orgId, quiet = false, confirmPublic = null }) {
|
|
166
185
|
const notes = [];
|
|
167
186
|
const say = (n) => { notes.push(n); if (quiet) return; if (n.kind === 'created') printEvent(events.created('quox', 'issue', n.url)); else if (n.kind === 'info') printInfo(n.text); else printWarning(n.text); };
|
|
168
187
|
const repoArg = options.repo || 'quoxai/quox';
|
|
169
188
|
const [owner, repo] = repoArg.includes('/') ? repoArg.split('/', 2) : ['quoxai', repoArg];
|
|
189
|
+
let confirmed = false;
|
|
190
|
+
if (options.public) {
|
|
191
|
+
confirmed = typeof confirmPublic === 'function' ? !!(await confirmPublic(`${owner}/${repo}`)) : false;
|
|
192
|
+
if (!confirmed) { say({ kind: 'warn', text: 'Not filed: public filing was not confirmed.' }); return { filed: false, notes }; }
|
|
193
|
+
}
|
|
170
194
|
const firstLine = complaint.split('\n')[0];
|
|
171
195
|
const title = firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
|
|
172
196
|
const ep = resolveEndpoint('github.issue.create');
|
|
@@ -180,6 +204,7 @@ export async function fileDeterministically({ client, complaint, options, orgId,
|
|
|
180
204
|
body: `## Gripe\n${complaint}\n\n## Context\n- Filed deterministically by \`quox gripe --yes\` (the GRIPE agent run did not file; quox#478 guarantee path)\n- Priority: minor / off-task observation\n\n## Suggested Action\nInvestigate and fix if confirmed.`,
|
|
181
205
|
labels: [options.label || 'fleet/qa'],
|
|
182
206
|
...(orgId ? { org_id: orgId } : {}),
|
|
207
|
+
...(confirmed ? { confirm_public: true } : {}),
|
|
183
208
|
},
|
|
184
209
|
});
|
|
185
210
|
const url = data?.issue?.html_url || data?.issue?.url || null;
|
|
@@ -228,7 +253,7 @@ const AUTH_UNAVAILABLE_MESSAGE =
|
|
|
228
253
|
* that is not there. The URL check stays first and stays authoritative, since
|
|
229
254
|
* a real issue link is the only thing that proves filing.
|
|
230
255
|
*/
|
|
231
|
-
export function classifyGripeResponse(response, { assumedYes = false, toolsUsed = null } = {}) {
|
|
256
|
+
export function classifyGripeResponse(response, { assumedYes = false, toolsUsed = null, complaint = '' } = {}) {
|
|
232
257
|
const text = String(response ?? '');
|
|
233
258
|
const url = (text.match(/https:\/\/github\.com\/\S+\/issues\/\d+/) || [])[0];
|
|
234
259
|
if (url) return { kind: 'filed', url, certain: true };
|
|
@@ -262,13 +287,13 @@ export function classifyGripeResponse(response, { assumedYes = false, toolsUsed
|
|
|
262
287
|
// issue; saying "not filed" would send the user to file a duplicate.
|
|
263
288
|
return { kind: 'filed-unknown', certain: true };
|
|
264
289
|
}
|
|
265
|
-
return { kind: 'not-filed', certain: true, hint: adviceHint(text, assumedYes) };
|
|
290
|
+
return { kind: 'not-filed', certain: true, hint: adviceHint(text, assumedYes, complaint) };
|
|
266
291
|
}
|
|
267
292
|
|
|
268
293
|
// No structured evidence available (an older collector, or a surface that
|
|
269
294
|
// does not return tools_used). Fall back to the prose-only classification,
|
|
270
295
|
// unchanged, rather than degrading every caller to "unknown".
|
|
271
|
-
const hint = adviceHint(text, assumedYes);
|
|
296
|
+
const hint = adviceHint(text, assumedYes, complaint);
|
|
272
297
|
if (hint === 'no-connector') return { kind: 'no-connector' };
|
|
273
298
|
if (hint === 'auth-unavailable') return { kind: 'auth-unavailable' };
|
|
274
299
|
if (hint === 'awaiting-confirm') return { kind: 'awaiting-confirm' };
|
|
@@ -282,7 +307,18 @@ export function classifyGripeResponse(response, { assumedYes = false, toolsUsed
|
|
|
282
307
|
*
|
|
283
308
|
* @returns {'no-connector'|'awaiting-confirm'|null}
|
|
284
309
|
*/
|
|
285
|
-
|
|
310
|
+
// The platform-failure shapes adviceHint matches first. Each is a global regex so a match can be blanked.
|
|
311
|
+
const AUTH_SIGNALS = [/membership/gi, /verification .{0,20}unavailable/gi, /auth service did not answer/gi, /\b(403|503|429)\b/g];
|
|
312
|
+
|
|
313
|
+
function adviceHint(text, assumedYes, complaint = '') {
|
|
314
|
+
// 2026-10-17: GRIPE's confirmation reply echoes the complaint, so a gripe ABOUT an HTTP 403 (or the membership
|
|
315
|
+
// page) read as a platform auth failure and "File it?" became "a platform authorization step failed". A signal
|
|
316
|
+
// counts only where the user did not write it: whatever the complaint itself contains is blanked first.
|
|
317
|
+
const userText = String(complaint || '');
|
|
318
|
+
let authText = text;
|
|
319
|
+
for (const re of AUTH_SIGNALS) {
|
|
320
|
+
for (const m of new Set(userText.match(re) || [])) authText = authText.split(m).join(' ');
|
|
321
|
+
}
|
|
286
322
|
// Platform-side authorization/availability failures beat the no-connector
|
|
287
323
|
// guess. On 2026-09-21 auth's rate limiter 429'd the collector's membership
|
|
288
324
|
// verify, the tool call 403'd with "no active membership", GRIPE narrated
|
|
@@ -290,12 +326,7 @@ function adviceHint(text, assumedYes) {
|
|
|
290
326
|
// "the org has no GitHub connector configured" — a confident falsehood while
|
|
291
327
|
// the vault held three healthy credentials (quox#565). These shapes come
|
|
292
328
|
// from the collector's own error strings, so match them FIRST.
|
|
293
|
-
if (
|
|
294
|
-
/membership/i.test(text) ||
|
|
295
|
-
/verification .{0,20}unavailable/i.test(text) ||
|
|
296
|
-
/auth service did not answer/i.test(text) ||
|
|
297
|
-
/\b(403|503|429)\b/.test(text)
|
|
298
|
-
) {
|
|
329
|
+
if (AUTH_SIGNALS.some((re) => { re.lastIndex = 0; return re.test(authText); })) {
|
|
299
330
|
return 'auth-unavailable';
|
|
300
331
|
}
|
|
301
332
|
|