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,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role commands: org custom roles (RBAC) + agent/tool permission rules
|
|
3
|
+
*
|
|
4
|
+
* quox role list|get|create|delete
|
|
5
|
+
* quox role capability add|remove
|
|
6
|
+
* quox role agent list|set|clear
|
|
7
|
+
* quox role tool list|set|clear
|
|
8
|
+
*
|
|
9
|
+
* Backend: services/auth/routes/customRoles.js, mounted at plain `/api`
|
|
10
|
+
* (org-scoped IN the URL: /api/orgs/:orgId/custom-roles...). Enterprise-tier
|
|
11
|
+
* + ORG_MANAGE_ROLES gated — expect 402/403 on non-enterprise orgs.
|
|
12
|
+
*
|
|
13
|
+
* Capability grants are subject to a server-side privilege-escalation
|
|
14
|
+
* guard: for capabilities in the known Capabilities enum, the caller must
|
|
15
|
+
* already hold the capability themselves or the grant is rejected with
|
|
16
|
+
* `forbidden_escalation` (customRoles.js:166-188). Free-form custom
|
|
17
|
+
* capability strings bypass this check (org-defined extensions).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { formatOutput, formatDetail, printSuccess, printInfo } from '../core/output.js';
|
|
21
|
+
import { resolveOrgId } from './helpers.js';
|
|
22
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
23
|
+
|
|
24
|
+
export function registerRoleCommands(program, ctx) {
|
|
25
|
+
const { client, auth } = ctx;
|
|
26
|
+
|
|
27
|
+
const role = program
|
|
28
|
+
.command('role')
|
|
29
|
+
.description('Manage org custom roles (enterprise — RBAC)');
|
|
30
|
+
|
|
31
|
+
// ── role list ────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
role
|
|
34
|
+
.command('list')
|
|
35
|
+
.description('List custom roles for the active org')
|
|
36
|
+
.action(async () => {
|
|
37
|
+
auth.requireAuth();
|
|
38
|
+
const orgId = resolveOrgId(auth, program);
|
|
39
|
+
const ep = resolveEndpoint('custom-roles.list', { orgId });
|
|
40
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
41
|
+
const rows = data.roles || data || [];
|
|
42
|
+
|
|
43
|
+
formatOutput({
|
|
44
|
+
columns: ['id', 'slug', 'name', 'description', 'created_at'],
|
|
45
|
+
headers: ['ID', 'Slug', 'Name', 'Description', 'Created'],
|
|
46
|
+
rows,
|
|
47
|
+
}, { format: program.opts().output, emptyMessage: 'No custom roles configured.' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── role get ─────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
role
|
|
53
|
+
.command('get')
|
|
54
|
+
.description('Show a custom role and its granted capabilities')
|
|
55
|
+
.argument('<roleId>', 'role ID')
|
|
56
|
+
.action(async (roleId) => {
|
|
57
|
+
auth.requireAuth();
|
|
58
|
+
const orgId = resolveOrgId(auth, program);
|
|
59
|
+
const ep = resolveEndpoint('custom-roles.get', { orgId, roleId });
|
|
60
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
61
|
+
|
|
62
|
+
formatDetail(data.role || {}, {
|
|
63
|
+
format: program.opts().output,
|
|
64
|
+
title: `Role: ${data.role?.name || roleId}`,
|
|
65
|
+
fields: ['id', 'slug', 'name', 'description', 'created_at'],
|
|
66
|
+
});
|
|
67
|
+
const capabilities = data.capabilities || [];
|
|
68
|
+
if (program.opts().output !== 'json') {
|
|
69
|
+
formatOutput({
|
|
70
|
+
columns: ['capability', 'scope_id'],
|
|
71
|
+
headers: ['Capability', 'Scope'],
|
|
72
|
+
rows: capabilities,
|
|
73
|
+
}, { format: program.opts().output, emptyMessage: 'No capabilities granted.' });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── role create ──────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
role
|
|
80
|
+
.command('create')
|
|
81
|
+
.description('Create a custom role')
|
|
82
|
+
.requiredOption('--slug <slug>', 'unique role slug')
|
|
83
|
+
.requiredOption('--name <name>', 'role name')
|
|
84
|
+
.option('--description <text>', 'role description')
|
|
85
|
+
.action(async (options) => {
|
|
86
|
+
auth.requireAuth();
|
|
87
|
+
const orgId = resolveOrgId(auth, program);
|
|
88
|
+
const body = { slug: options.slug, name: options.name };
|
|
89
|
+
if (options.description) body.description = options.description;
|
|
90
|
+
|
|
91
|
+
const ep = resolveEndpoint('custom-roles.create', { orgId });
|
|
92
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
93
|
+
printSuccess(`Role "${options.name}" created.`);
|
|
94
|
+
formatDetail(data.role || data, { format: program.opts().output });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── role delete ──────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
role
|
|
100
|
+
.command('delete')
|
|
101
|
+
.description('Delete a custom role')
|
|
102
|
+
.argument('<roleId>', 'role ID')
|
|
103
|
+
.option('--force', 'skip confirmation')
|
|
104
|
+
.action(async (roleId, options) => {
|
|
105
|
+
auth.requireAuth();
|
|
106
|
+
const orgId = resolveOrgId(auth, program);
|
|
107
|
+
|
|
108
|
+
if (!options.force && !program.opts().yes) {
|
|
109
|
+
const { default: inquirer } = await import('inquirer');
|
|
110
|
+
const ans = await inquirer.prompt([{
|
|
111
|
+
type: 'confirm',
|
|
112
|
+
name: 'confirm',
|
|
113
|
+
message: `Delete role ${roleId}? Members assigned this role lose its capabilities immediately.`,
|
|
114
|
+
default: false,
|
|
115
|
+
}]);
|
|
116
|
+
if (!ans.confirm) {
|
|
117
|
+
printInfo('Cancelled.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ep = resolveEndpoint('custom-roles.delete', { orgId, roleId });
|
|
123
|
+
await client.delete(ep.path, { service: ep.service });
|
|
124
|
+
printSuccess('Role deleted.');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── role capability ─────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
const capability = role
|
|
130
|
+
.command('capability')
|
|
131
|
+
.description('Manage capability grants on a role');
|
|
132
|
+
|
|
133
|
+
capability
|
|
134
|
+
.command('add')
|
|
135
|
+
.description('Grant a capability to a role')
|
|
136
|
+
.argument('<roleId>', 'role ID')
|
|
137
|
+
.argument('<capability>', 'capability string (e.g. "agents.manage")')
|
|
138
|
+
.option('--scope-id <scopeId>', 'restrict the grant to a specific scope')
|
|
139
|
+
.action(async (roleId, cap, options) => {
|
|
140
|
+
auth.requireAuth();
|
|
141
|
+
const orgId = resolveOrgId(auth, program);
|
|
142
|
+
const body = { capability: cap };
|
|
143
|
+
if (options.scopeId) body.scope_id = options.scopeId;
|
|
144
|
+
|
|
145
|
+
const ep = resolveEndpoint('custom-roles.capabilities.add', { orgId, roleId });
|
|
146
|
+
await client.post(ep.path, { service: ep.service, body });
|
|
147
|
+
printSuccess(`Capability "${cap}" granted.`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
capability
|
|
151
|
+
.command('remove')
|
|
152
|
+
.description('Revoke a capability from a role')
|
|
153
|
+
.argument('<roleId>', 'role ID')
|
|
154
|
+
.argument('<capability>', 'capability string')
|
|
155
|
+
.option('--scope-id <scopeId>', 'scope ID (must match the grant\'s scope)')
|
|
156
|
+
.action(async (roleId, cap, options) => {
|
|
157
|
+
auth.requireAuth();
|
|
158
|
+
const orgId = resolveOrgId(auth, program);
|
|
159
|
+
const query = { capability: cap };
|
|
160
|
+
if (options.scopeId) query.scope_id = options.scopeId;
|
|
161
|
+
|
|
162
|
+
const ep = resolveEndpoint('custom-roles.capabilities.remove', { orgId, roleId });
|
|
163
|
+
await client.delete(ep.path, { service: ep.service, query });
|
|
164
|
+
printSuccess(`Capability "${cap}" revoked.`);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── role agent ───────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
const agent = role
|
|
170
|
+
.command('agent')
|
|
171
|
+
.description('Manage agent access rules on a role');
|
|
172
|
+
|
|
173
|
+
agent
|
|
174
|
+
.command('list')
|
|
175
|
+
.description('List agent access rules for a role')
|
|
176
|
+
.argument('<roleId>', 'role ID')
|
|
177
|
+
.action(async (roleId) => {
|
|
178
|
+
auth.requireAuth();
|
|
179
|
+
const orgId = resolveOrgId(auth, program);
|
|
180
|
+
const ep = resolveEndpoint('custom-roles.agents.list', { orgId, roleId });
|
|
181
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
182
|
+
const rows = data.permissions || data || [];
|
|
183
|
+
|
|
184
|
+
formatOutput({
|
|
185
|
+
columns: ['agent_id', 'scope_id', 'allowed'],
|
|
186
|
+
headers: ['Agent ID', 'Scope', 'Allowed'],
|
|
187
|
+
rows,
|
|
188
|
+
}, { format: program.opts().output, emptyMessage: 'No agent access rules set for this role.' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
agent
|
|
192
|
+
.command('set')
|
|
193
|
+
.description('Set (allow or deny) a role\'s access to an agent')
|
|
194
|
+
.argument('<roleId>', 'role ID')
|
|
195
|
+
.argument('<agentId>', 'agent ID')
|
|
196
|
+
.requiredOption('--allowed <bool>', 'true|false')
|
|
197
|
+
.option('--scope-id <scopeId>', 'restrict the rule to a specific scope')
|
|
198
|
+
.action(async (roleId, agentId, options) => {
|
|
199
|
+
auth.requireAuth();
|
|
200
|
+
const orgId = resolveOrgId(auth, program);
|
|
201
|
+
if (options.allowed !== 'true' && options.allowed !== 'false') {
|
|
202
|
+
throw new Error('--allowed must be "true" or "false".');
|
|
203
|
+
}
|
|
204
|
+
const body = { agent_id: agentId, allowed: options.allowed === 'true' };
|
|
205
|
+
if (options.scopeId) body.scope_id = options.scopeId;
|
|
206
|
+
|
|
207
|
+
const ep = resolveEndpoint('custom-roles.agents.set', { orgId, roleId });
|
|
208
|
+
await client.put(ep.path, { service: ep.service, body });
|
|
209
|
+
printSuccess(`Agent access rule set (${body.allowed ? 'allowed' : 'denied'}).`);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
agent
|
|
213
|
+
.command('clear')
|
|
214
|
+
.description('Remove an agent access rule from a role')
|
|
215
|
+
.argument('<roleId>', 'role ID')
|
|
216
|
+
.argument('<agentId>', 'agent ID')
|
|
217
|
+
.option('--scope-id <scopeId>', 'scope ID (must match the rule\'s scope)')
|
|
218
|
+
.action(async (roleId, agentId, options) => {
|
|
219
|
+
auth.requireAuth();
|
|
220
|
+
const orgId = resolveOrgId(auth, program);
|
|
221
|
+
const query = { agent_id: agentId };
|
|
222
|
+
if (options.scopeId) query.scope_id = options.scopeId;
|
|
223
|
+
|
|
224
|
+
const ep = resolveEndpoint('custom-roles.agents.clear', { orgId, roleId });
|
|
225
|
+
await client.delete(ep.path, { service: ep.service, query });
|
|
226
|
+
printSuccess('Agent access rule cleared.');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ── role tool ────────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
const tool = role
|
|
232
|
+
.command('tool')
|
|
233
|
+
.description('Manage tool access rules on a role');
|
|
234
|
+
|
|
235
|
+
tool
|
|
236
|
+
.command('list')
|
|
237
|
+
.description('List tool access rules for a role')
|
|
238
|
+
.argument('<roleId>', 'role ID')
|
|
239
|
+
.action(async (roleId) => {
|
|
240
|
+
auth.requireAuth();
|
|
241
|
+
const orgId = resolveOrgId(auth, program);
|
|
242
|
+
const ep = resolveEndpoint('custom-roles.tools.list', { orgId, roleId });
|
|
243
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
244
|
+
const rows = data.permissions || data || [];
|
|
245
|
+
|
|
246
|
+
formatOutput({
|
|
247
|
+
columns: ['tool_id', 'allowed'],
|
|
248
|
+
headers: ['Tool ID', 'Allowed'],
|
|
249
|
+
rows,
|
|
250
|
+
}, { format: program.opts().output, emptyMessage: 'No tool access rules set for this role.' });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
tool
|
|
254
|
+
.command('set')
|
|
255
|
+
.description('Set (allow or deny) a role\'s access to a tool')
|
|
256
|
+
.argument('<roleId>', 'role ID')
|
|
257
|
+
.argument('<toolId>', 'tool ID')
|
|
258
|
+
.requiredOption('--allowed <bool>', 'true|false')
|
|
259
|
+
.action(async (roleId, toolId, options) => {
|
|
260
|
+
auth.requireAuth();
|
|
261
|
+
const orgId = resolveOrgId(auth, program);
|
|
262
|
+
if (options.allowed !== 'true' && options.allowed !== 'false') {
|
|
263
|
+
throw new Error('--allowed must be "true" or "false".');
|
|
264
|
+
}
|
|
265
|
+
const body = { tool_id: toolId, allowed: options.allowed === 'true' };
|
|
266
|
+
|
|
267
|
+
const ep = resolveEndpoint('custom-roles.tools.set', { orgId, roleId });
|
|
268
|
+
await client.put(ep.path, { service: ep.service, body });
|
|
269
|
+
printSuccess(`Tool access rule set (${body.allowed ? 'allowed' : 'denied'}).`);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
tool
|
|
273
|
+
.command('clear')
|
|
274
|
+
.description('Remove a tool access rule from a role')
|
|
275
|
+
.argument('<roleId>', 'role ID')
|
|
276
|
+
.argument('<toolId>', 'tool ID')
|
|
277
|
+
.action(async (roleId, toolId) => {
|
|
278
|
+
auth.requireAuth();
|
|
279
|
+
const orgId = resolveOrgId(auth, program);
|
|
280
|
+
const query = { tool_id: toolId };
|
|
281
|
+
|
|
282
|
+
const ep = resolveEndpoint('custom-roles.tools.clear', { orgId, roleId });
|
|
283
|
+
await client.delete(ep.path, { service: ep.service, query });
|
|
284
|
+
printSuccess('Tool access rule cleared.');
|
|
285
|
+
});
|
|
286
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run commands: list, get, create, cancel, resume
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatOutput, formatDetail, printSuccess, printWarning } from '../core/output.js';
|
|
6
|
+
import { resolveOrgId, parseJSON } from './helpers.js';
|
|
7
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
|
|
9
|
+
export function registerRunCommands(program, ctx) {
|
|
10
|
+
const { client, auth } = ctx;
|
|
11
|
+
|
|
12
|
+
const run = program
|
|
13
|
+
.command('run')
|
|
14
|
+
.description('Manage workflow runs');
|
|
15
|
+
|
|
16
|
+
// fleet #465: runs live in two stores. The engine (quoxflow) is the
|
|
17
|
+
// execution store `workflow run`/apply-triggered runs actually land in;
|
|
18
|
+
// the auth-service 'runs.*' store is a separate model quoxflow never
|
|
19
|
+
// writes to. `list`/`get` below try the engine FIRST and fall back to the
|
|
20
|
+
// auth-service store, labeling which store answered.
|
|
21
|
+
|
|
22
|
+
run
|
|
23
|
+
.command('list')
|
|
24
|
+
.description('List runs')
|
|
25
|
+
.option('--status <status>', 'filter by status')
|
|
26
|
+
.option('--workflow <id>', 'filter by workflow')
|
|
27
|
+
.option('--limit <n>', 'max results', '50')
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
auth.requireAuth();
|
|
30
|
+
const orgId = resolveOrgId(auth, program);
|
|
31
|
+
|
|
32
|
+
let rows, source;
|
|
33
|
+
try {
|
|
34
|
+
const ep = resolveEndpoint('executions.list');
|
|
35
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
36
|
+
const items = data.data || data.items || (Array.isArray(data) ? data : []);
|
|
37
|
+
rows = items.map((item) => ({ ...item, source: 'engine' }));
|
|
38
|
+
source = 'engine';
|
|
39
|
+
} catch (err) {
|
|
40
|
+
printWarning(`Engine (quoxflow) execution list failed, falling back to the auth-service store: ${err.message}`);
|
|
41
|
+
const query = { limit: options.limit };
|
|
42
|
+
if (options.status) query.status = options.status;
|
|
43
|
+
if (options.workflow) query.workflow_id = options.workflow;
|
|
44
|
+
const ep = resolveEndpoint('runs.list', { orgId });
|
|
45
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
46
|
+
const items = data.items || data.runs || (Array.isArray(data) ? data : []);
|
|
47
|
+
rows = items.map((item) => ({ ...item, source: 'auth' }));
|
|
48
|
+
source = 'auth';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
formatOutput({
|
|
52
|
+
// fleet #392: runs table has finished_at, not completed_at.
|
|
53
|
+
columns: ['id', 'workflow_name', 'status', 'source', 'started_at', 'finished_at'],
|
|
54
|
+
headers: ['ID', 'Workflow', 'Status', 'Source', 'Started', 'Completed'],
|
|
55
|
+
rows,
|
|
56
|
+
}, { format: program.opts().output, emptyMessage: `No results (${source} store).` });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
run
|
|
60
|
+
.command('get')
|
|
61
|
+
.description('Get run details')
|
|
62
|
+
.argument('<runId>', 'run ID')
|
|
63
|
+
.action(async (runId) => {
|
|
64
|
+
auth.requireAuth();
|
|
65
|
+
const orgId = resolveOrgId(auth, program);
|
|
66
|
+
|
|
67
|
+
let data, source;
|
|
68
|
+
try {
|
|
69
|
+
const ep = resolveEndpoint('executions.get', { executionId: runId });
|
|
70
|
+
const res = await client.get(ep.path, { service: ep.service });
|
|
71
|
+
data = res.data || res;
|
|
72
|
+
source = 'engine';
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err.status !== 404) throw err;
|
|
75
|
+
try {
|
|
76
|
+
const ep = resolveEndpoint('runs.get', { orgId, pollRunId: runId });
|
|
77
|
+
const res = await client.get(ep.path, { service: ep.service });
|
|
78
|
+
data = res.run || res;
|
|
79
|
+
source = 'auth';
|
|
80
|
+
} catch (err2) {
|
|
81
|
+
if (err2.status !== 404) throw err2;
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Run "${runId}" was not found in either the engine (quoxflow) execution store or the auth-service run store.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
formatDetail({ ...data, source }, { format: program.opts().output });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
run
|
|
92
|
+
.command('create')
|
|
93
|
+
.description('Start a new workflow run')
|
|
94
|
+
.requiredOption('--workflow <id>', 'workflow ID')
|
|
95
|
+
.option('--input <json>', 'input data as JSON')
|
|
96
|
+
.action(async (options) => {
|
|
97
|
+
auth.requireAuth();
|
|
98
|
+
const orgId = resolveOrgId(auth, program);
|
|
99
|
+
const body = { workflow_id: options.workflow };
|
|
100
|
+
if (options.input) body.context = parseJSON(options.input, '--input');
|
|
101
|
+
const data = await client.post(resolveEndpoint('runs.create', { orgId }).path, { service: resolveEndpoint('runs.create', { orgId }).service, body });
|
|
102
|
+
printSuccess(`Run started: ${data.run?.id || data.id || 'ok'}`);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
run
|
|
106
|
+
.command('cancel')
|
|
107
|
+
.description('Cancel a running workflow')
|
|
108
|
+
.argument('<runId>', 'run ID')
|
|
109
|
+
.option('--reason <text>', 'cancellation reason')
|
|
110
|
+
.action(async (runId, options) => {
|
|
111
|
+
auth.requireAuth();
|
|
112
|
+
const orgId = resolveOrgId(auth, program);
|
|
113
|
+
const body = {};
|
|
114
|
+
if (options.reason) body.reason = options.reason;
|
|
115
|
+
await client.post(resolveEndpoint('runs.cancel', { orgId, runId }).path, { service: resolveEndpoint('runs.cancel', { orgId, runId }).service, body });
|
|
116
|
+
printSuccess('Run cancelled.');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
run
|
|
120
|
+
.command('resume')
|
|
121
|
+
.description('Resume a paused run')
|
|
122
|
+
.argument('<runId>', 'run ID')
|
|
123
|
+
.option('--response <json>', 'human response payload as JSON')
|
|
124
|
+
.action(async (runId, options) => {
|
|
125
|
+
auth.requireAuth();
|
|
126
|
+
const orgId = resolveOrgId(auth, program);
|
|
127
|
+
const body = {};
|
|
128
|
+
if (options.response) body.response = parseJSON(options.response, '--response');
|
|
129
|
+
await client.post(resolveEndpoint('runs.resume', { orgId, runId }).path, { service: resolveEndpoint('runs.resume', { orgId, runId }).service, body });
|
|
130
|
+
printSuccess('Run resumed.');
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety commands: kill switch (per-agent E-STOP) + instance DEFCON.
|
|
3
|
+
*
|
|
4
|
+
* quox safety status org halt state: DEFCON + active E-STOPs
|
|
5
|
+
* quox safety gate <agentId> the enforcement verdict a dispatcher sees
|
|
6
|
+
* quox safety estop <agentId> activate the per-agent kill switch
|
|
7
|
+
* quox safety release <agentId> release it
|
|
8
|
+
* quox safety defcon <LEVEL> set instance DEFCON (GREEN..RED)
|
|
9
|
+
* quox safety history append-only audit trail of every flip
|
|
10
|
+
*
|
|
11
|
+
* Backend: collector routes/safetyGate.js (quox#503, PR #505; dispatch
|
|
12
|
+
* chokepoint coverage quox#506). Mutations REQUIRE a human admin login:
|
|
13
|
+
* the server rejects agent/resident/service tokens by design, so if
|
|
14
|
+
* `estop`/`release`/`defcon` return 403 admin_required under a resident
|
|
15
|
+
* profile, that is the governance boundary working: log in as a human
|
|
16
|
+
* (`quox login`), do not look for a bypass.
|
|
17
|
+
*
|
|
18
|
+
* Closes the quox#507 parity gap: the 2026-09-02 live E-STOP proof drive
|
|
19
|
+
* had to be scripted with raw curl because these verbs did not exist.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { formatOutput, formatDetail, printSuccess, printWarning } from '../core/output.js';
|
|
23
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
24
|
+
|
|
25
|
+
export function registerSafetyCommands(program, ctx) {
|
|
26
|
+
const { client, auth } = ctx;
|
|
27
|
+
|
|
28
|
+
const safety = program
|
|
29
|
+
.command('safety')
|
|
30
|
+
.description('Kill switch (per-agent E-STOP) and instance DEFCON (human-admin gated mutations)');
|
|
31
|
+
|
|
32
|
+
safety
|
|
33
|
+
.command('status')
|
|
34
|
+
.description('Org halt state: DEFCON level + active per-agent E-STOPs')
|
|
35
|
+
.action(async () => {
|
|
36
|
+
auth.requireAuth();
|
|
37
|
+
const ep = resolveEndpoint('safety.status');
|
|
38
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
39
|
+
formatDetail(data, { format: program.opts().output });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
safety
|
|
43
|
+
.command('gate')
|
|
44
|
+
.description('The enforcement verdict a dispatcher gets for this agent right now')
|
|
45
|
+
.argument('<agentId>', 'agent id, e.g. quox, sentinel')
|
|
46
|
+
.action(async (agentId) => {
|
|
47
|
+
auth.requireAuth();
|
|
48
|
+
const ep = resolveEndpoint('safety.gate', { agentId });
|
|
49
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
50
|
+
formatDetail(data, { format: program.opts().output });
|
|
51
|
+
if (data.blocked) printWarning(`Dispatch for ${agentId} is BLOCKED: ${data.reason}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
safety
|
|
55
|
+
.command('estop')
|
|
56
|
+
.description('Activate the per-agent kill switch (HUMAN admin only, enforced server-side)')
|
|
57
|
+
.argument('<agentId>', 'agent id to halt')
|
|
58
|
+
.option('--reason <text>', 'why (lands in the audit trail)')
|
|
59
|
+
.action(async (agentId, options) => {
|
|
60
|
+
auth.requireAuth();
|
|
61
|
+
const ep = resolveEndpoint('safety.estop', { agentId });
|
|
62
|
+
const data = await client.post(ep.path, { service: ep.service, body: { reason: options.reason || null } });
|
|
63
|
+
printSuccess(`E-STOP ACTIVE for ${agentId}${data.was_active ? ' (was already active)' : ''}: new dispatch is blocked at every chokepoint; live loop sessions end at the next turn boundary.`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
safety
|
|
67
|
+
.command('release')
|
|
68
|
+
.description('Release a per-agent kill switch (HUMAN admin only, enforced server-side)')
|
|
69
|
+
.argument('<agentId>', 'agent id to release')
|
|
70
|
+
.action(async (agentId) => {
|
|
71
|
+
auth.requireAuth();
|
|
72
|
+
const ep = resolveEndpoint('safety.release', { agentId });
|
|
73
|
+
await client.delete(ep.path, { service: ep.service });
|
|
74
|
+
printSuccess(`E-STOP released for ${agentId}.`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
safety
|
|
78
|
+
.command('defcon')
|
|
79
|
+
.description('Set instance DEFCON: GREEN | BLUE | AMBER | ORANGE (escalate) | RED (full halt)')
|
|
80
|
+
.argument('<level>', 'GREEN, BLUE, AMBER, ORANGE or RED')
|
|
81
|
+
.option('--reason <text>', 'why (lands in the audit trail)')
|
|
82
|
+
.action(async (level, options) => {
|
|
83
|
+
auth.requireAuth();
|
|
84
|
+
const ep = resolveEndpoint('safety.defcon');
|
|
85
|
+
const data = await client.post(ep.path, { service: ep.service, body: { level: level.toUpperCase(), reason: options.reason || null } });
|
|
86
|
+
printSuccess(`DEFCON ${data.old_level} -> ${data.new_level}.`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
safety
|
|
90
|
+
.command('history')
|
|
91
|
+
.description('Append-only audit trail of every E-STOP and DEFCON change')
|
|
92
|
+
.option('--agent <agentId>', 'filter to one agent')
|
|
93
|
+
.option('--limit <n>', 'max rows', '50')
|
|
94
|
+
.action(async (options) => {
|
|
95
|
+
auth.requireAuth();
|
|
96
|
+
const ep = resolveEndpoint('safety.history');
|
|
97
|
+
const qs = new URLSearchParams();
|
|
98
|
+
if (options.agent) qs.set('agent_id', options.agent);
|
|
99
|
+
qs.set('limit', options.limit);
|
|
100
|
+
const data = await client.get(`${ep.path}?${qs}`, { service: ep.service });
|
|
101
|
+
formatOutput({
|
|
102
|
+
columns: ['created_at', 'scope', 'action', 'agent_id', 'new_value', 'actor_label', 'reason'],
|
|
103
|
+
headers: ['At', 'Scope', 'Action', 'Agent', 'Value', 'Actor', 'Reason'],
|
|
104
|
+
rows: data.history || [],
|
|
105
|
+
}, { format: program.opts().output, emptyMessage: 'No safety state changes recorded.' });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCIM commands: org SCIM bearer tokens (IdP-driven directory sync auth)
|
|
3
|
+
*
|
|
4
|
+
* quox scim tokens list|create|delete
|
|
5
|
+
*
|
|
6
|
+
* Backend: services/auth/routes/webhooks.js, mounted at plain `/api`
|
|
7
|
+
* (org-scoped IN the URL: /api/orgs/:orgId/scim/tokens...). Enterprise-tier
|
|
8
|
+
* + ORG_MANAGE_SETTINGS gated — expect 402/403 on non-enterprise orgs.
|
|
9
|
+
*
|
|
10
|
+
* The plaintext bearer token is returned ONLY on create — it is never
|
|
11
|
+
* retrievable again after that response (only its SHA-256 is stored). We
|
|
12
|
+
* print it prominently and warn the user to record it now.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { formatOutput, printSuccess, printWarning } from '../core/output.js';
|
|
16
|
+
import { resolveOrgId } from './helpers.js';
|
|
17
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
18
|
+
|
|
19
|
+
function printToken(token) {
|
|
20
|
+
printWarning('Record this bearer token now — it will not be shown again:');
|
|
21
|
+
console.log(` ${token}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function registerScimCommands(program, ctx) {
|
|
25
|
+
const { client, auth } = ctx;
|
|
26
|
+
|
|
27
|
+
const scim = program
|
|
28
|
+
.command('scim')
|
|
29
|
+
.description('Manage org SCIM bearer tokens (enterprise — IdP directory sync)');
|
|
30
|
+
|
|
31
|
+
const tokens = scim
|
|
32
|
+
.command('tokens')
|
|
33
|
+
.description('Manage SCIM bearer tokens');
|
|
34
|
+
|
|
35
|
+
// ── scim tokens list ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
tokens
|
|
38
|
+
.command('list')
|
|
39
|
+
.description('List SCIM tokens for the active org')
|
|
40
|
+
.action(async () => {
|
|
41
|
+
auth.requireAuth();
|
|
42
|
+
const orgId = resolveOrgId(auth, program);
|
|
43
|
+
const ep = resolveEndpoint('scim.tokens.list', { orgId });
|
|
44
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
45
|
+
const rows = data.tokens || data || [];
|
|
46
|
+
|
|
47
|
+
formatOutput({
|
|
48
|
+
columns: ['id', 'label', 'created_at', 'last_used_at', 'revoked_at'],
|
|
49
|
+
headers: ['ID', 'Label', 'Created', 'Last Used', 'Revoked'],
|
|
50
|
+
rows,
|
|
51
|
+
}, { format: program.opts().output, emptyMessage: 'No SCIM tokens configured.' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── scim tokens create ──────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
tokens
|
|
57
|
+
.command('create')
|
|
58
|
+
.description('Create a SCIM bearer token')
|
|
59
|
+
.option('--label <label>', 'human-readable label')
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
auth.requireAuth();
|
|
62
|
+
const orgId = resolveOrgId(auth, program);
|
|
63
|
+
const body = {};
|
|
64
|
+
if (options.label) body.label = options.label;
|
|
65
|
+
|
|
66
|
+
const ep = resolveEndpoint('scim.tokens.create', { orgId });
|
|
67
|
+
// NOTE: the backend response here is the raw `{ id, token }` shape,
|
|
68
|
+
// not wrapped under a named key like every other create route in
|
|
69
|
+
// this CLI — see endpoints/scim.js's header note.
|
|
70
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
71
|
+
printSuccess(`SCIM token created (id: ${data.id}).`);
|
|
72
|
+
if (data.token) printToken(data.token);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── scim tokens delete ──────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
tokens
|
|
78
|
+
.command('delete')
|
|
79
|
+
.description('Revoke a SCIM token')
|
|
80
|
+
.argument('<tokenId>', 'token ID')
|
|
81
|
+
.action(async (tokenId) => {
|
|
82
|
+
auth.requireAuth();
|
|
83
|
+
const orgId = resolveOrgId(auth, program);
|
|
84
|
+
const ep = resolveEndpoint('scim.tokens.delete', { orgId, tokenId });
|
|
85
|
+
await client.delete(ep.path, { service: ep.service });
|
|
86
|
+
printSuccess('SCIM token revoked.');
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope commands
|
|
3
|
+
*
|
|
4
|
+
* quox scope list
|
|
5
|
+
*
|
|
6
|
+
* Scopes are read-only projections of the org/division/department/team
|
|
7
|
+
* hierarchy — there is no backend write route for scopes. To create,
|
|
8
|
+
* update, or delete a scope, use `quox org`, `quox division`,
|
|
9
|
+
* `quox department`, or `quox team` instead.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { formatOutput } from '../core/output.js';
|
|
13
|
+
import { resolveOrgId } from './helpers.js';
|
|
14
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
15
|
+
|
|
16
|
+
export function registerScopeCommands(program, ctx) {
|
|
17
|
+
const { client, auth } = ctx;
|
|
18
|
+
|
|
19
|
+
const scope = program
|
|
20
|
+
.command('scope')
|
|
21
|
+
.description('View scopes (read-only — use `quox org`/`quox division`/`quox team` to manage)');
|
|
22
|
+
|
|
23
|
+
scope
|
|
24
|
+
.command('list')
|
|
25
|
+
.description('List scopes')
|
|
26
|
+
.action(async () => {
|
|
27
|
+
auth.requireAuth();
|
|
28
|
+
const orgId = resolveOrgId(auth, program);
|
|
29
|
+
// fleet #392: the route returns a nested {tree} unless flat=true is
|
|
30
|
+
// sent, and it was never sent — so `data.scopes` was undefined,
|
|
31
|
+
// `data` fell through as the object {tree: ...}, and `.map()` threw
|
|
32
|
+
// TypeError. This command crashed rather than merely showing dashes.
|
|
33
|
+
// Columns also renamed to the real ones (name_cache/scope_type);
|
|
34
|
+
// `resources` has no counterpart at all, so `path` and `depth` are
|
|
35
|
+
// shown instead of inventing a zero.
|
|
36
|
+
const data = await client.get(resolveEndpoint('scopes.list', { orgId }).path, {
|
|
37
|
+
service: resolveEndpoint('scopes.list', { orgId }).service,
|
|
38
|
+
query: { flat: 'true' },
|
|
39
|
+
});
|
|
40
|
+
const scopes = Array.isArray(data.scopes) ? data.scopes : [];
|
|
41
|
+
// fleet #399.3: `depth` is a scope-TYPE ordinal (services/auth/
|
|
42
|
+
// migrations/000-baseline.sql: "0=org, 1=division, 2=department,
|
|
43
|
+
// 3=team"), not a count of path segments below root. A team can
|
|
44
|
+
// legitimately show Depth 3 while its `path` reads only two levels
|
|
45
|
+
// deep. Header made self-explanatory rather than recomputing depth.
|
|
46
|
+
formatOutput({
|
|
47
|
+
columns: ['id', 'name', 'type', 'path', 'depth'],
|
|
48
|
+
headers: ['ID', 'Name', 'Type', 'Path', 'Depth (0=org,1=div,2=dept,3=team)'],
|
|
49
|
+
rows: scopes.map(s => ({
|
|
50
|
+
id: s.id,
|
|
51
|
+
name: s.name_cache,
|
|
52
|
+
type: s.scope_type,
|
|
53
|
+
path: s.path,
|
|
54
|
+
depth: s.depth,
|
|
55
|
+
})),
|
|
56
|
+
}, { format: program.opts().output, emptyMessage: 'No scopes found.' });
|
|
57
|
+
});
|
|
58
|
+
}
|