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,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev commands — DEVSUITE-HUB Phase 2.
|
|
3
|
+
*
|
|
4
|
+
* `quox dev enable` is the CLI surface for the "register as dev server"
|
|
5
|
+
* trigger: constructs, signs, and delivers a quoxagent dev.CapabilityProfile
|
|
6
|
+
* onto a target fleet host via the collector's
|
|
7
|
+
* POST /api/internal/dev-profiles/enable (see
|
|
8
|
+
* services/collector/lib/devCapabilityProfileIssuer.js). Only
|
|
9
|
+
* `dev.register` is granted by default, matching what's real today in
|
|
10
|
+
* quoxagent's dev plugin family (internal/plugins/dev) — pass
|
|
11
|
+
* `--capabilities` to grant additional dev.* names.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { formatDetail, formatOutput, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
15
|
+
import { shortLicenceLabel, matchesLicenceFilter } from './skillRightsFormat.js';
|
|
16
|
+
import { resolveOrgId } from './helpers.js';
|
|
17
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
18
|
+
import { QuoxApiError } from '../core/errors.js';
|
|
19
|
+
import { registerDevSupervisorCommands } from './devSupervisor.js';
|
|
20
|
+
import { registerDevResidentCommands } from './devResident.js';
|
|
21
|
+
|
|
22
|
+
export function registerDevCommands(program, ctx) {
|
|
23
|
+
const { client, auth, config } = ctx;
|
|
24
|
+
|
|
25
|
+
const dev = program
|
|
26
|
+
.command('dev')
|
|
27
|
+
.description('Manage dev-mode (DEVSUITE) fleet hosts');
|
|
28
|
+
|
|
29
|
+
dev
|
|
30
|
+
.command('enable')
|
|
31
|
+
.description('Register a fleet host as a dev server (signs + delivers a quoxagent capability profile)')
|
|
32
|
+
.argument('<hostId>', 'target host_id (matches the running quoxagent\'s agent.host_id)')
|
|
33
|
+
.option('--capabilities <list>', 'comma-separated dev.* capability names (default: dev.register)')
|
|
34
|
+
.option('--max-concurrent-sessions <n>', 'max concurrent dev.sessions tmux sessions', (v) => parseInt(v, 10))
|
|
35
|
+
.option('--ttl-seconds <n>', 'profile expiry, in seconds from now (default: no expiry)', (v) => parseInt(v, 10))
|
|
36
|
+
.option('--no-wait-pack', 'do not wait for the skill-pack install to be confirmed')
|
|
37
|
+
.action(async (hostId, options) => {
|
|
38
|
+
auth.requireAuth();
|
|
39
|
+
const orgId = resolveOrgId(auth, program);
|
|
40
|
+
|
|
41
|
+
// fleet #472: the collector's dev-enable admin gate evaluates the
|
|
42
|
+
// JWT's ACTIVE org, not the org named by --org, so a mismatched flag
|
|
43
|
+
// is silently mischecked and 403s even the target org's own owner.
|
|
44
|
+
// Until the server gate can check membership in the TARGET org,
|
|
45
|
+
// reject the mismatch loudly and name the working path.
|
|
46
|
+
const flagOrg = program.opts().org;
|
|
47
|
+
const sessionOrg = auth.getOrgId();
|
|
48
|
+
if (flagOrg && sessionOrg && flagOrg !== sessionOrg) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`--org ${flagOrg} differs from your active org context (${sessionOrg}). ` +
|
|
51
|
+
`The dev-enable admin check runs against the ACTIVE org, so this request would 403 ` +
|
|
52
|
+
`even for the target org's owner (fleet #472). ` +
|
|
53
|
+
`Run 'quox org switch ${flagOrg}' first, then re-run dev enable.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const body = { host_id: hostId, org_id: orgId };
|
|
58
|
+
// quox#455: tell the provisioner which edge URL this working admin
|
|
59
|
+
// session actually uses, so the resident bootstrap lands with a real
|
|
60
|
+
// instance_url. A loopback hostname is fine to send: the collector
|
|
61
|
+
// swaps it for the host-reachable address server-side.
|
|
62
|
+
const enableBaseUrl = typeof config.getBaseUrl === 'function' ? config.getBaseUrl() : null;
|
|
63
|
+
if (enableBaseUrl) body.instance_url = enableBaseUrl;
|
|
64
|
+
if (options.capabilities) {
|
|
65
|
+
body.capabilities = options.capabilities.split(',').map((c) => c.trim()).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
if (Number.isInteger(options.maxConcurrentSessions)) {
|
|
68
|
+
body.max_concurrent_sessions = options.maxConcurrentSessions;
|
|
69
|
+
}
|
|
70
|
+
if (Number.isInteger(options.ttlSeconds)) {
|
|
71
|
+
body.ttl_seconds = options.ttlSeconds;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const endpoint = resolveEndpoint('v1.dev.profiles.enable');
|
|
75
|
+
let data;
|
|
76
|
+
try {
|
|
77
|
+
data = await client.post(endpoint.path, { service: endpoint.service, body });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// quoxbastion#12 / quox#588: against a bastion in approval mode 'risky'
|
|
80
|
+
// the collector relays a "present a collector-signed
|
|
81
|
+
// X-Bastion-Approval-Grant" refusal, but there is no CLI path yet to
|
|
82
|
+
// mint/present that grant. Turn the dead-end 502 into an actionable
|
|
83
|
+
// failure rather than a raw relayed error the operator can't act on.
|
|
84
|
+
const detail = `${err?.message || ''} ${(() => { try { return JSON.stringify(err?.body || ''); } catch { return ''; } })()}`;
|
|
85
|
+
if (/X-Bastion-Approval-Grant|approval required/i.test(detail)) {
|
|
86
|
+
printWarning(`Bastion is in approval mode 'risky': dev enable needs a collector-signed approval grant, and the CLI cannot mint or present one yet.`);
|
|
87
|
+
printInfo(`Known gap, tracked as quoxai/quox#588 (and quoxbastion#12) — the HITL grant-mint + auto-retry path is not built.`);
|
|
88
|
+
printInfo(`Until it ships, a bastion/org admin can lower this bastion's approval mode below 'risky', then re-run:`);
|
|
89
|
+
printInfo(` quox dev enable ${hostId}`);
|
|
90
|
+
}
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
formatDetail(data, { format: program.opts().output, title: `Dev profile delivered to ${hostId}` });
|
|
95
|
+
if (data && data.delivered) {
|
|
96
|
+
printSuccess(`${hostId} registered as a dev server (capabilities: ${(data.capabilities || []).join(', ')}).`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2026-08-19: DELIVERED IS NOT LIVE. quoxagent loads the signed dev
|
|
100
|
+
// capability profile ONCE AT STARTUP (internal/agent/agent.go, see
|
|
101
|
+
// LoadDevCapabilityProfile) with no reload and no watch, so a profile
|
|
102
|
+
// that lands on disk is not adopted until the agent restarts. This
|
|
103
|
+
// command happily printed "registered as a dev server (capabilities:
|
|
104
|
+
// dev.register, dev.build, dev.sessions, dev.plan)" while the host went
|
|
105
|
+
// on reporting the previous three, on a heartbeat seconds old. The
|
|
106
|
+
// capability pill did not render and nothing said why.
|
|
107
|
+
//
|
|
108
|
+
// Exactly the bug the skill-pack block below was written to kill, one
|
|
109
|
+
// layer up: "success" meant the WRITE succeeded, never that the thing
|
|
110
|
+
// took effect. So do what that block does — go and look.
|
|
111
|
+
if (data && data.delivered && Array.isArray(data.capabilities) && data.capabilities.length) {
|
|
112
|
+
const signed = [...data.capabilities].sort();
|
|
113
|
+
let adopted = null;
|
|
114
|
+
try {
|
|
115
|
+
const listEp = resolveEndpoint('v1.dev.servers.list');
|
|
116
|
+
const deadline = Date.now() + 30000;
|
|
117
|
+
while (Date.now() < deadline) {
|
|
118
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
119
|
+
let list = null;
|
|
120
|
+
try {
|
|
121
|
+
// org_id is REQUIRED: the dev-servers handler fails closed rather
|
|
122
|
+
// than falling back to an unscoped list (fixed 2026-08-19, it used
|
|
123
|
+
// to list across orgs). Omitting it here got a refusal, which this
|
|
124
|
+
// block then reported as "registry unreadable".
|
|
125
|
+
list = await client.get(listEp.path, { service: listEp.service, query: { org_id: orgId } });
|
|
126
|
+
} catch { list = null; }
|
|
127
|
+
const rows = Array.isArray(list?.servers) ? list.servers : [];
|
|
128
|
+
const row = rows.find((h) => (h.host_id || h.hostId) === hostId);
|
|
129
|
+
const caps = row && (row.enabled_capabilities || row.enabledCapabilities);
|
|
130
|
+
const parsed = typeof caps === 'string' ? JSON.parse(caps) : caps;
|
|
131
|
+
if (Array.isArray(parsed)) {
|
|
132
|
+
adopted = [...parsed].sort();
|
|
133
|
+
if (signed.every((c) => adopted.includes(c))) break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch { adopted = null; }
|
|
137
|
+
|
|
138
|
+
if (adopted === null) {
|
|
139
|
+
console.error(`Could not confirm ${hostId} adopted the new profile (registry unreadable). Verify with: quox dev doctor ${hostId}`);
|
|
140
|
+
} else {
|
|
141
|
+
const missing = signed.filter((c) => !adopted.includes(c));
|
|
142
|
+
if (missing.length) {
|
|
143
|
+
// Phase 7: the collector restarts the agent itself now (see
|
|
144
|
+
// agent_refresh in the response), so a lingering mismatch means
|
|
145
|
+
// either the restart was deferred for live sessions, or
|
|
146
|
+
// something on the host is genuinely wrong.
|
|
147
|
+
const refresh = data.agent_refresh || {};
|
|
148
|
+
if (refresh.deferred_live_sessions > 0) {
|
|
149
|
+
console.error(
|
|
150
|
+
`NOT LIVE YET: ${hostId} still reports [${adopted.join(', ')}]. The agent restart was deferred because ` +
|
|
151
|
+
`${refresh.deferred_live_sessions} live session(s) would die with it. The profile and binary are delivered; ` +
|
|
152
|
+
`the next agent restart adopts them. Force it when the sessions are done:\n` +
|
|
153
|
+
` systemctl --user restart quoxagent (user service on dev VMs)\n` +
|
|
154
|
+
`Then confirm with: quox dev doctor ${hostId}`
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
// #462 (footgun 4, exampleestate-website 2026-08-22): the most
|
|
158
|
+
// common cause of delivered+restarted+still-empty is the agent
|
|
159
|
+
// being UNABLE TO VERIFY the profile: the signing-key fetch
|
|
160
|
+
// needs org/collector/service-key env in the unit. The
|
|
161
|
+
// collector's verifier-env leg stamps the two non-secret vars
|
|
162
|
+
// and reports whether the seam key file exists; name that
|
|
163
|
+
// diagnosis here instead of sending the operator to the
|
|
164
|
+
// journal blind.
|
|
165
|
+
let hint = `Check the agent journal on the host, then: quox dev doctor ${hostId}`;
|
|
166
|
+
if (refresh.seam_key === 'missing') {
|
|
167
|
+
hint = `DIAGNOSIS: the host has no seam service key file, so the agent cannot fetch the org signing key ` +
|
|
168
|
+
`and cannot verify the profile (profile_verified stays false). Provision the key file the env drop-in ` +
|
|
169
|
+
`points at (see quoxpert-dev-servers footgun 4), then restart the agent.`;
|
|
170
|
+
} else if (refresh.verifier_env === 'error') {
|
|
171
|
+
hint = `DIAGNOSIS: stamping the signing-key-fetch env onto the unit failed (${refresh.verifier_env_error || 'unknown'}). ` +
|
|
172
|
+
`Without QUOXAGENT_ORG_ID and QUOXAGENT_COLLECTOR_URL in the agent unit the profile cannot be verified.`;
|
|
173
|
+
}
|
|
174
|
+
console.error(
|
|
175
|
+
`NOT LIVE: ${hostId} still reports [${adopted.join(', ')}] and has not adopted [${missing.join(', ')}] ` +
|
|
176
|
+
`even though the enable ${refresh.restarted ? 'restarted the agent' : 'could not restart the agent'} ` +
|
|
177
|
+
`(agent_refresh: ${JSON.stringify(refresh)}). ${hint}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
printSuccess(`Host has adopted the profile: ${adopted.join(', ')}.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// DEVSUITE-SKILLS P6f: the pack install fires async behind enable, and
|
|
187
|
+
// "success" used to mean only the profile landed — the arg-limit bug
|
|
188
|
+
// shipped broken packs behind a green enable for exactly that reason.
|
|
189
|
+
// Poll the durable pack status until confirmed (or say plainly that it
|
|
190
|
+
// is not confirmed and name the follow-up verb).
|
|
191
|
+
if (data && data.delivered && options.waitPack !== false) {
|
|
192
|
+
const statusEp = resolveEndpoint('v1.dev.skills.status', { hostId });
|
|
193
|
+
const deadline = Date.now() + 90000;
|
|
194
|
+
process.stdout.write('Waiting for skill-pack install confirmation');
|
|
195
|
+
let pack = null;
|
|
196
|
+
while (Date.now() < deadline) {
|
|
197
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
198
|
+
process.stdout.write('.');
|
|
199
|
+
try {
|
|
200
|
+
pack = await client.get(statusEp.path, { service: statusEp.service });
|
|
201
|
+
} catch { pack = null; }
|
|
202
|
+
if (pack && pack.status && pack.status !== 'unknown') break;
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write('\n');
|
|
205
|
+
if (pack && (pack.status === 'current' || pack.installedVersion)) {
|
|
206
|
+
printSuccess(`Skill pack v${pack.installedVersion || '?'} confirmed on ${hostId} (${pack.status}).`);
|
|
207
|
+
} else {
|
|
208
|
+
console.error(`Skill pack NOT confirmed on ${hostId} within 90s — the profile is live, the pack is not proven. Check with: quox dev skills status ${hostId} (or force it: quox dev skills sync ${hostId})`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// QUOXPERTICUS P5: skill-pack parity (the pack routes existed with no verb).
|
|
214
|
+
const skills = dev
|
|
215
|
+
.command('skills')
|
|
216
|
+
.description('Dev skill-pack delivery (signed core dev pack)');
|
|
217
|
+
|
|
218
|
+
skills
|
|
219
|
+
.command('status')
|
|
220
|
+
.description('Show the installed pack version/hash on a dev host vs current, with SkillRights labels')
|
|
221
|
+
.argument('<hostId>', 'target host_id')
|
|
222
|
+
.option('--rights [licence]', 'list per-skill SkillRights declarations; optionally filter by open|notrain|reserved|undeclared or a full identifier')
|
|
223
|
+
.action(async (hostId, options) => {
|
|
224
|
+
auth.requireAuth();
|
|
225
|
+
const ep = resolveEndpoint('v1.dev.skills.status', { hostId });
|
|
226
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
227
|
+
|
|
228
|
+
// SKILLRIGHTS #478 phase 11 (pricing-free): rights are load-bearing
|
|
229
|
+
// data, so render them deliberately rather than dumping the raw
|
|
230
|
+
// object. Summary always; per-skill table behind --rights, with an
|
|
231
|
+
// optional licence filter (the CLI half of the marketplace's
|
|
232
|
+
// "licence filter").
|
|
233
|
+
const { rights, installedRights, ...core } = data;
|
|
234
|
+
formatDetail(core, { format: program.opts().output, title: `Skill pack status: ${hostId}` });
|
|
235
|
+
if (rights) {
|
|
236
|
+
const byLicence = Object.entries(rights.byLicense || {})
|
|
237
|
+
.map(([id, n]) => `${shortLicenceLabel(id)} x${n}`)
|
|
238
|
+
.join(', ');
|
|
239
|
+
printInfo(`Rights: ${rights.declared}/${rights.total} declared${byLicence ? ` (${byLicence})` : ''}${rights.undeclared ? `, ${rights.undeclared} undeclared` : ''}.`);
|
|
240
|
+
}
|
|
241
|
+
if (options.rights) {
|
|
242
|
+
const filter = typeof options.rights === 'string' ? options.rights : null;
|
|
243
|
+
const entries = (rights && rights.entries) || [];
|
|
244
|
+
const rows = entries
|
|
245
|
+
.filter((e) => matchesLicenceFilter(e.license, filter))
|
|
246
|
+
.map((e) => ({ skill: e.ref, licence: shortLicenceLabel(e.license) }));
|
|
247
|
+
formatOutput({
|
|
248
|
+
columns: ['skill', 'licence'],
|
|
249
|
+
headers: ['Skill', 'Licence'],
|
|
250
|
+
rows,
|
|
251
|
+
}, { format: program.opts().output, emptyMessage: filter ? `No skills matching licence filter '${filter}'.` : 'No rights entries.' });
|
|
252
|
+
if (installedRights && Array.isArray(installedRights)) {
|
|
253
|
+
const drifted = entries.length !== installedRights.length
|
|
254
|
+
|| entries.some((e) => {
|
|
255
|
+
const got = installedRights.find((i) => i.ref === e.ref);
|
|
256
|
+
return !got || got.license !== e.license;
|
|
257
|
+
});
|
|
258
|
+
if (drifted) printWarning('Declared rights differ from what this host RECEIVED at last confirmed sync (installedRights); re-sync to update the host-side rights manifest.');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
skills
|
|
264
|
+
.command('sync')
|
|
265
|
+
.description('Install or update the signed core dev pack on a dev host')
|
|
266
|
+
.argument('<hostId>', 'target host_id')
|
|
267
|
+
.action(async (hostId) => {
|
|
268
|
+
auth.requireAuth();
|
|
269
|
+
const ep = resolveEndpoint('v1.dev.skills.sync', { hostId });
|
|
270
|
+
let data;
|
|
271
|
+
try {
|
|
272
|
+
data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
273
|
+
} catch (err) {
|
|
274
|
+
// SKILLRIGHTS #478 Phase 11: under the require-acceptance policy the
|
|
275
|
+
// collector refuses the download until a person has accepted the
|
|
276
|
+
// terms. Name the exact command rather than leaving a bare 409: an
|
|
277
|
+
// unactionable refusal is how a governance control becomes a thing
|
|
278
|
+
// people route around.
|
|
279
|
+
if (err && err.body && err.body.error === 'acceptance_required') {
|
|
280
|
+
printWarning(`Refused: nobody in this org has accepted the current terms for pack '${err.body.packId}'.`);
|
|
281
|
+
printInfo(`Accept them (as a user, not a service credential), then re-run the sync:`);
|
|
282
|
+
printInfo(` quox dev skills accept ${err.body.packId}`);
|
|
283
|
+
printInfo(` quox dev skills sync ${hostId}`);
|
|
284
|
+
}
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
const { rightsNotice, ...detail } = data;
|
|
288
|
+
formatDetail(detail, { format: program.opts().output, title: `Skill pack sync: ${hostId}` });
|
|
289
|
+
// The distribution terms travel with every sync (phase 11 contract
|
|
290
|
+
// enforcement): show them, do not bury them in the JSON.
|
|
291
|
+
if (rightsNotice) printInfo(rightsNotice);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// SKILLRIGHTS #478 Phase 11 — AFFIRMATIVE ACCEPTANCE. The whole point of
|
|
295
|
+
// this command is that a human reads the terms and says yes: it PRINTS the
|
|
296
|
+
// terms in full, then requires an interactive confirmation or an explicit
|
|
297
|
+
// --i-accept. It never accepts silently, and a service credential is
|
|
298
|
+
// refused server-side (user_assent_required), so automation cannot stand in
|
|
299
|
+
// for a person here.
|
|
300
|
+
skills
|
|
301
|
+
.command('accept')
|
|
302
|
+
.description('Read the QuoxSkills distribution terms for a pack and record your acceptance of them')
|
|
303
|
+
.argument('<packId>', 'pack id (e.g. quox-core-dev-pack)')
|
|
304
|
+
.option('--i-accept', 'accept without the interactive prompt (still records YOU as the accepting user)')
|
|
305
|
+
.action(async (packId, options) => {
|
|
306
|
+
auth.requireAuth();
|
|
307
|
+
|
|
308
|
+
const offerEp = resolveEndpoint('skills.acceptance.offer', { packId });
|
|
309
|
+
const offer = await client.get(offerEp.path, { service: offerEp.service });
|
|
310
|
+
|
|
311
|
+
// The terms, in full. Not a link, not a summary: an acceptance of text
|
|
312
|
+
// the actor was never shown is not evidence of anything.
|
|
313
|
+
printInfo(`Terms for pack '${offer.packId}'${offer.packVersion ? ` (version ${offer.packVersion})` : ''}:`);
|
|
314
|
+
console.log('');
|
|
315
|
+
console.log(offer.termsBytes);
|
|
316
|
+
console.log('');
|
|
317
|
+
|
|
318
|
+
const rights = offer.rights || {};
|
|
319
|
+
const byLicence = Object.entries(rights.byLicense || {})
|
|
320
|
+
.map(([id, n]) => `${shortLicenceLabel(id)} x${n}`)
|
|
321
|
+
.join(', ');
|
|
322
|
+
printInfo(`Declared rights: ${rights.declared ?? 0}/${rights.total ?? 0} declared${byLicence ? ` (${byLicence})` : ''}${rights.undeclared ? `, ${rights.undeclared} undeclared` : ''}.`);
|
|
323
|
+
printInfo(`Pack content hash: ${offer.contentHash}`);
|
|
324
|
+
printInfo(`Declared rights hash: ${offer.rightsSha256}`);
|
|
325
|
+
printInfo(`Terms hash: ${offer.termsSha256} (presentation ${offer.presentationVersion})`);
|
|
326
|
+
|
|
327
|
+
if (!options.iAccept) {
|
|
328
|
+
const { default: inquirer } = await import('inquirer');
|
|
329
|
+
const ans = await inquirer.prompt([{
|
|
330
|
+
type: 'confirm',
|
|
331
|
+
name: 'confirm',
|
|
332
|
+
message: `Do you accept these terms, on behalf of your organisation, for exactly this pack content?`,
|
|
333
|
+
default: false,
|
|
334
|
+
}]);
|
|
335
|
+
if (!ans.confirm) {
|
|
336
|
+
printInfo('Not accepted. Nothing was recorded.');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const recordEp = resolveEndpoint('skills.acceptance.record');
|
|
342
|
+
const result = await client.post(recordEp.path, {
|
|
343
|
+
service: recordEp.service,
|
|
344
|
+
body: {
|
|
345
|
+
challenge: offer.challenge,
|
|
346
|
+
termsSha256: offer.termsSha256,
|
|
347
|
+
contentHash: offer.contentHash,
|
|
348
|
+
rightsSha256: offer.rightsSha256,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
printSuccess(`Acceptance recorded (id ${result.acceptanceId}).`);
|
|
353
|
+
// The honest claim, verbatim from the server, plus its limits. No
|
|
354
|
+
// surface may say more than this sentence says.
|
|
355
|
+
printInfo(result.honestClaim);
|
|
356
|
+
if (result.claimLimits) printInfo(result.claimLimits);
|
|
357
|
+
if (!result.envelopeId) {
|
|
358
|
+
printWarning('No evidence envelope was recorded for this acceptance (transcript storage disabled?). The acceptance row stands; the AEE reference does not.');
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// SLOTS-TMUX step B: interactive tmux sessions on a dev host. quoxagent's
|
|
363
|
+
// five dev.sessions methods had NO caller (note #2877): no collector route,
|
|
364
|
+
// no quox verb. These are the verbs. Each one is a single short signed
|
|
365
|
+
// envelope delivered over Bastion; the session itself lives on the host's
|
|
366
|
+
// tmux server between calls, so it survives detach and outlives any one
|
|
367
|
+
// command here.
|
|
368
|
+
const sessions = dev
|
|
369
|
+
.command('sessions')
|
|
370
|
+
.description('Interactive tmux sessions on a dev host (survive detach; drive with send/pane)');
|
|
371
|
+
|
|
372
|
+
sessions
|
|
373
|
+
.command('list')
|
|
374
|
+
.description('List live tmux sessions on a dev host')
|
|
375
|
+
.argument('<hostId>', 'target host_id')
|
|
376
|
+
.action(async (hostId) => {
|
|
377
|
+
auth.requireAuth();
|
|
378
|
+
const ep = resolveEndpoint('v1.dev.sessions.list', { hostId });
|
|
379
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
380
|
+
formatDetail(data, { format: program.opts().output, title: `tmux sessions: ${hostId}` });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
sessions
|
|
384
|
+
.command('new')
|
|
385
|
+
.description('Create a tmux session on a dev host')
|
|
386
|
+
.argument('<hostId>', 'target host_id')
|
|
387
|
+
.option('--name <name>', 'session name (default: agent-generated)')
|
|
388
|
+
.option('--backend <backend>', 'what runs in the pane: shell | claude | codex', 'shell')
|
|
389
|
+
.action(async (hostId, options) => {
|
|
390
|
+
auth.requireAuth();
|
|
391
|
+
const ep = resolveEndpoint('v1.dev.sessions.new', { hostId });
|
|
392
|
+
const body = { backend: options.backend };
|
|
393
|
+
if (options.name) body.name = options.name;
|
|
394
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
395
|
+
formatDetail(data, { format: program.opts().output, title: `tmux session created on ${hostId}` });
|
|
396
|
+
if (data && data.session_id) {
|
|
397
|
+
printSuccess(`Session ${data.session_id} (${data.name || 'unnamed'}) is live. Drive it with:`);
|
|
398
|
+
printInfo(` quox dev sessions send ${hostId} ${data.session_id} "<command>"`);
|
|
399
|
+
printInfo(` quox dev sessions pane ${hostId} ${data.session_id}`);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// quox#451: `new --name X` makes X the tmux session NAME, but send/pane/kill
|
|
404
|
+
// resolved only the agent's internal id, so reusing the name you just typed
|
|
405
|
+
// 422'd "session not found". On exactly that refusal, retry once by name.
|
|
406
|
+
// An explicit --by-name skips straight to the name lookup, as before.
|
|
407
|
+
async function withNameFallback(byName, call) {
|
|
408
|
+
try {
|
|
409
|
+
return await call(byName);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
const said = [err && err.message, err && err.code, err && err.body && err.body.error].filter(Boolean).join(' ');
|
|
412
|
+
if (!byName && err instanceof QuoxApiError && err.status === 422 && /session not found/i.test(said)) {
|
|
413
|
+
return call(true);
|
|
414
|
+
}
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
sessions
|
|
420
|
+
.command('send')
|
|
421
|
+
.description('Send one line of input to a session (Enter is appended)')
|
|
422
|
+
.argument('<hostId>', 'target host_id')
|
|
423
|
+
.argument('<sessionId>', 'session id from `new` or `list` (or tmux name with --by-name)')
|
|
424
|
+
.argument('<keys>', 'the line to type into the pane')
|
|
425
|
+
.option('--by-name', 'treat the identifier as the tmux session NAME (orphaned sessions have no id)')
|
|
426
|
+
.action(async (hostId, sessionId, keys, options) => {
|
|
427
|
+
auth.requireAuth();
|
|
428
|
+
const data = await withNameFallback(!!options.byName, (byName) => {
|
|
429
|
+
const ep = resolveEndpoint('v1.dev.sessions.keys', { hostId, sessionId, byName });
|
|
430
|
+
return client.post(ep.path, { service: ep.service, body: { keys } });
|
|
431
|
+
});
|
|
432
|
+
if (data && data.sent) printSuccess(`Sent to ${sessionId} on ${hostId}.`);
|
|
433
|
+
else formatDetail(data, { format: program.opts().output });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
sessions
|
|
437
|
+
.command('pane')
|
|
438
|
+
.description('Read the current pane contents of a session')
|
|
439
|
+
.argument('<hostId>', 'target host_id')
|
|
440
|
+
.argument('<sessionId>', 'session id (or tmux session name with --by-name)')
|
|
441
|
+
.option('--by-name', 'treat the identifier as the tmux session NAME (orphaned sessions have no id)')
|
|
442
|
+
.action(async (hostId, sessionId, options) => {
|
|
443
|
+
auth.requireAuth();
|
|
444
|
+
const data = await withNameFallback(!!options.byName, (byName) => {
|
|
445
|
+
const ep = resolveEndpoint('v1.dev.sessions.pane', { hostId, sessionId, byName });
|
|
446
|
+
return client.get(ep.path, { service: ep.service });
|
|
447
|
+
});
|
|
448
|
+
if (program.opts().output === 'json') {
|
|
449
|
+
formatDetail(data, { format: 'json' });
|
|
450
|
+
} else {
|
|
451
|
+
// The pane IS the output: print it raw, trailing blank lines trimmed.
|
|
452
|
+
process.stdout.write(String(data && data.pane || '').replace(/\n+$/, '') + '\n');
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
sessions
|
|
457
|
+
.command('kill')
|
|
458
|
+
.description('Kill a tmux session on a dev host')
|
|
459
|
+
.argument('<hostId>', 'target host_id')
|
|
460
|
+
.argument('<sessionId>', 'session id (or tmux session name with --by-name)')
|
|
461
|
+
.option('--by-name', 'treat the identifier as the tmux session NAME (orphaned sessions have no id)')
|
|
462
|
+
.action(async (hostId, sessionId, options) => {
|
|
463
|
+
auth.requireAuth();
|
|
464
|
+
const data = await withNameFallback(!!options.byName, (byName) => {
|
|
465
|
+
const ep = resolveEndpoint('v1.dev.sessions.kill', { hostId, sessionId, byName });
|
|
466
|
+
return client.delete(ep.path, { service: ep.service });
|
|
467
|
+
});
|
|
468
|
+
if (data && data.killed) printSuccess(`Session ${sessionId} killed on ${hostId}.`);
|
|
469
|
+
else formatDetail(data, { format: program.opts().output });
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// DEVSUITE phase 9: "a quox dev doctor verb (pack version, rules block, MCP
|
|
473
|
+
// health, session freshness) so 'is it even taking?' has a product answer".
|
|
474
|
+
//
|
|
475
|
+
// The question this exists to kill: an operator runs `quox dev enable`, gets
|
|
476
|
+
// a success line, and then has no way to tell whether the host is actually a
|
|
477
|
+
// working dev server. Every check below reads REAL registry state and reports
|
|
478
|
+
// what it found; nothing is inferred from the enable having returned 200,
|
|
479
|
+
// because a 200 from the enable is exactly the evidence that was already
|
|
480
|
+
// insufficient.
|
|
481
|
+
//
|
|
482
|
+
// Read-only by construction. A doctor that changes things cannot be run
|
|
483
|
+
// freely when you are unsure, which is precisely when you want to run it.
|
|
484
|
+
dev
|
|
485
|
+
.command('doctor')
|
|
486
|
+
.description('Check whether a host is really working as a dev server')
|
|
487
|
+
.argument('[hostId]', 'target host_id (default: every registered dev server)')
|
|
488
|
+
.action(async (hostId) => {
|
|
489
|
+
auth.requireAuth();
|
|
490
|
+
const orgId = resolveOrgId(auth, program);
|
|
491
|
+
|
|
492
|
+
const listEp = resolveEndpoint('v1.dev.servers.list');
|
|
493
|
+
const list = await client.get(listEp.path, { service: listEp.service, query: { org_id: orgId } });
|
|
494
|
+
const all = Array.isArray(list?.servers) ? list.servers : [];
|
|
495
|
+
const targets = hostId ? all.filter((s) => s.host_id === hostId) : all;
|
|
496
|
+
|
|
497
|
+
if (hostId && targets.length === 0) {
|
|
498
|
+
// Do not say "unhealthy": the host is not registered to this org at
|
|
499
|
+
// all, which is a different problem with a different fix.
|
|
500
|
+
printWarning(`${hostId} is not a registered dev server for this org.`);
|
|
501
|
+
printInfo(' Register it with `quox dev enable ' + hostId + '`.');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (targets.length === 0) {
|
|
505
|
+
printWarning('No dev servers are registered for this org.');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const report = [];
|
|
510
|
+
for (const s of targets) {
|
|
511
|
+
const findings = [];
|
|
512
|
+
|
|
513
|
+
// 1. Did the signed capability profile verify? An unverified profile
|
|
514
|
+
// means the host is registered but not trusted to act.
|
|
515
|
+
if (!s.profile_verified) findings.push('profile not verified');
|
|
516
|
+
|
|
517
|
+
// 2. Is it still beating? A host that registered once and went quiet
|
|
518
|
+
// looks identical to a working one in a plain listing.
|
|
519
|
+
const ageMs = s.last_seen_at ? Date.now() - Number(s.last_seen_at) : null;
|
|
520
|
+
const staleMin = ageMs === null ? null : Math.round(ageMs / 60000);
|
|
521
|
+
if (staleMin === null) findings.push('never seen');
|
|
522
|
+
else if (staleMin > 15) findings.push(`last seen ${staleMin}m ago`);
|
|
523
|
+
|
|
524
|
+
// 3. Did anything actually get enabled? A profile with no capabilities
|
|
525
|
+
// is a dev server that can do nothing.
|
|
526
|
+
const enabled = Array.isArray(s.enabled_capabilities) ? s.enabled_capabilities : [];
|
|
527
|
+
if (enabled.length === 0) findings.push('no capabilities enabled');
|
|
528
|
+
|
|
529
|
+
// 3b. Did resident identity provisioning (quox#453 phase 15) land?
|
|
530
|
+
// A host with capabilities enabled but no resident identity can
|
|
531
|
+
// still deliver profiles, but nothing that needs the resident
|
|
532
|
+
// keypair (host-native signing) works. resident_state is only
|
|
533
|
+
// checked when the host has capabilities at all, matching #3's
|
|
534
|
+
// gate: an unenabled host has nothing resident identity is for
|
|
535
|
+
// yet.
|
|
536
|
+
if (enabled.length > 0 && s.resident_state !== 'provisioned') {
|
|
537
|
+
let finding = 'resident identity not provisioned (re-run quox dev enable)';
|
|
538
|
+
if (s.resident_state === 'failed' && s.resident_detail) {
|
|
539
|
+
finding += ` (${s.resident_detail})`;
|
|
540
|
+
}
|
|
541
|
+
findings.push(finding);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 4. Is the skill pack on and current? This is the half of "enable"
|
|
545
|
+
// that silently does not happen, and the reason the verb exists.
|
|
546
|
+
let pack = null;
|
|
547
|
+
try {
|
|
548
|
+
const packEp = resolveEndpoint('v1.dev.skills.status', { hostId: s.host_id });
|
|
549
|
+
pack = await client.get(packEp.path, { service: packEp.service });
|
|
550
|
+
if (pack?.status === 'missing') findings.push('skill pack not installed');
|
|
551
|
+
else if (pack?.drifted) findings.push(`skill pack v${pack.installedVersion} behind v${pack.currentVersion}`);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
// Unknown, not broken. Saying "pack missing" because a probe failed
|
|
554
|
+
// is the class of lie this whole verb is meant to prevent.
|
|
555
|
+
findings.push(`skill pack status unknown (${err.message})`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 5. Headless supervisor (quox#453 phase 15 follow-on, DEVSUITE-
|
|
559
|
+
// DEVMODE) — read-only status line, NOT a health finding. Most
|
|
560
|
+
// hosts legitimately run with the supervisor off; the switch is a
|
|
561
|
+
// human decision (server-side requireHumanAdmin() gate on the
|
|
562
|
+
// write endpoint, `quox dev supervisor` doctor's write-free
|
|
563
|
+
// sibling), not something this read-only verb should flag as a
|
|
564
|
+
// problem either way.
|
|
565
|
+
let supervisor = null;
|
|
566
|
+
try {
|
|
567
|
+
const supEp = resolveEndpoint('v1.dev.supervisor.get', { hostId: s.host_id });
|
|
568
|
+
const sup = await client.get(supEp.path, { service: supEp.service });
|
|
569
|
+
supervisor = { enabled: !!sup?.enabled, slot: sup?.slot || null };
|
|
570
|
+
} catch (err) {
|
|
571
|
+
// Unknown, not off. Same "don't invent a finding from a probe
|
|
572
|
+
// failure" posture as the skill-pack status check above.
|
|
573
|
+
supervisor = { enabled: null, slot: null, error: err.message };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
report.push({
|
|
577
|
+
host_id: s.host_id,
|
|
578
|
+
healthy: findings.length === 0,
|
|
579
|
+
capabilities: enabled.length,
|
|
580
|
+
pack: pack ? (pack.status || 'unknown') : 'unknown',
|
|
581
|
+
last_seen_minutes: staleMin,
|
|
582
|
+
supervisor,
|
|
583
|
+
findings,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (program.opts().output === 'json') {
|
|
588
|
+
formatDetail({ dev_servers: report }, { format: 'json' });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (const r of report) {
|
|
593
|
+
if (r.healthy) {
|
|
594
|
+
printSuccess(`${r.host_id}: ready (${r.capabilities} capabilities, pack ${r.pack})`);
|
|
595
|
+
} else {
|
|
596
|
+
printWarning(`${r.host_id}: ${r.findings.length} problem${r.findings.length === 1 ? '' : 's'}`);
|
|
597
|
+
for (const f of r.findings) printInfo(` - ${f}`);
|
|
598
|
+
}
|
|
599
|
+
const sup = r.supervisor;
|
|
600
|
+
if (sup && sup.error) {
|
|
601
|
+
printInfo(` supervisor: unknown (${sup.error})`);
|
|
602
|
+
} else if (sup && sup.enabled) {
|
|
603
|
+
printInfo(` supervisor: enabled${sup.slot ? ` (slot ${sup.slot})` : ''}`);
|
|
604
|
+
} else {
|
|
605
|
+
printInfo(' supervisor: disabled');
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// DEVSUITE-DEVMODE (quox#453 phase 15 follow-on) — `quox dev supervisor`
|
|
611
|
+
// (see lib/commands/devSupervisor.js). Headless: a resident agent answers
|
|
612
|
+
// its slot channel with no interactive session open.
|
|
613
|
+
registerDevSupervisorCommands(dev, ctx);
|
|
614
|
+
|
|
615
|
+
// QUOXPERTICUS-REVISIT P3 — `quox dev resident` (see
|
|
616
|
+
// lib/commands/devResident.js). A FULL INTERACTIVE claude session as the
|
|
617
|
+
// resident, not the headless supervisor above: deploy/reboot/retire it
|
|
618
|
+
// through the governed tmux + slots planes, no ssh.
|
|
619
|
+
registerDevResidentCommands(dev, ctx);
|
|
620
|
+
}
|