quoxcli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/core/config.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, renameSync, openSync, closeSync, statSync, unlinkSync, copyFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
10
|
+
import { homedir, networkInterfaces, hostname as osHostname } from 'node:os';
|
|
11
11
|
|
|
12
12
|
function getConfigDir() { return join(homedir(), '.config', 'quox'); }
|
|
13
13
|
function getConfigFile() { return join(getConfigDir(), 'config.json'); }
|
|
@@ -94,35 +94,118 @@ function isPrivateHost(hostname) {
|
|
|
94
94
|
return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
// QUOXON-Z2H lane R (2026-10-14): where the edge (nginx on the profile's base
|
|
98
|
+
// URL) serves each compose service. Mirrors nginx.conf: /api/auth, /api/setup,
|
|
99
|
+
// /api/orgs ... go to auth at the SAME path, so auth's prefix is empty.
|
|
100
|
+
export const EDGE_PATHS = {
|
|
101
|
+
auth: '',
|
|
102
|
+
collector: '/collector',
|
|
103
|
+
memory: '/api/memory',
|
|
104
|
+
tasks: '/tasks',
|
|
105
|
+
orchestrator: '/orchestrator',
|
|
106
|
+
quoxflow: '/quoxflow',
|
|
107
|
+
quoxbrain: '/api/quoxbrain',
|
|
108
|
+
files: '/files-api',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// The local edge's default port. A loopback base on this port is the stack on
|
|
112
|
+
// THIS box, whose services are reachable on their plain-http loopback binds.
|
|
113
|
+
// Any other loopback port is a tunnel (ssh -L 13105:vm:3000) to another box.
|
|
114
|
+
const LOCAL_EDGE_PORTS = new Set(['3000']);
|
|
115
|
+
|
|
116
|
+
function localIdentity(opts = {}) {
|
|
117
|
+
let localAddrs = opts.localAddrs;
|
|
118
|
+
if (!localAddrs) {
|
|
119
|
+
localAddrs = [];
|
|
120
|
+
try {
|
|
121
|
+
for (const list of Object.values(networkInterfaces())) {
|
|
122
|
+
for (const a of list || []) localAddrs.push(a.address);
|
|
123
|
+
}
|
|
124
|
+
} catch { /* no interfaces readable: treat nothing as local */ }
|
|
125
|
+
}
|
|
126
|
+
let host = opts.hostname;
|
|
127
|
+
if (host === undefined) { try { host = osHostname(); } catch { host = ''; } }
|
|
128
|
+
return { localAddrs: new Set(localAddrs), hostname: String(host || '').toLowerCase() };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isThisMachine(hostname, id) {
|
|
132
|
+
const h = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
133
|
+
if (LOOPBACK_HOSTS.has(h) || LOOPBACK_HOSTS.has(`[${h}]`)) return true;
|
|
134
|
+
if (id.localAddrs.has(h)) return true;
|
|
135
|
+
return !!id.hostname && (h === id.hostname || h === `${id.hostname}.local`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* True when a service URL lands on THIS machine (loopback bind, or a base whose
|
|
140
|
+
* host is one of this box's own addresses) rather than another host or a tunnel
|
|
141
|
+
* to one. Used to keep this box's ambient QUOX_SERVICE_KEY from being offered
|
|
142
|
+
* to a remote instance.
|
|
143
|
+
*/
|
|
144
|
+
export function isLocalServiceTarget(serviceUrl, opts = {}) {
|
|
145
|
+
try {
|
|
146
|
+
const u = new URL(serviceUrl);
|
|
147
|
+
const id = localIdentity(opts);
|
|
148
|
+
if (LOOPBACK_HOSTS.has(u.hostname)) {
|
|
149
|
+
// Loopback is this box when it is the local edge or a compose service
|
|
150
|
+
// bind; any other loopback port is a tunnel to another box.
|
|
151
|
+
const port = u.port || (u.protocol === 'https:' ? '443' : '80');
|
|
152
|
+
return LOCAL_EDGE_PORTS.has(port) || Object.values(SERVICE_PORTS).map(String).includes(port);
|
|
153
|
+
}
|
|
154
|
+
return isThisMachine(u.hostname, id);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Join an absolute or relative path onto a service URL WITHOUT dropping its path prefix (new URL('/x', base) would). */
|
|
161
|
+
export function joinServiceUrl(serviceUrl, path) {
|
|
162
|
+
const base = new URL(serviceUrl);
|
|
163
|
+
const prefix = base.pathname.replace(/\/+$/, '');
|
|
164
|
+
const rel = new URL(String(path).replace(/^\/*/, '/'), 'http://x');
|
|
165
|
+
base.pathname = `${prefix}${rel.pathname}`;
|
|
166
|
+
base.search = rel.search;
|
|
167
|
+
base.hash = '';
|
|
168
|
+
return base;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Derivation of a compose service's URL from a profile base URL (normally the
|
|
172
|
+
// ACTIVE profile's own `url`, never a global default).
|
|
102
173
|
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
174
|
+
// QUOXON-Z2H lane R: the base URL is the single source of truth. Port guessing
|
|
175
|
+
// is ONLY for the stack on this very machine:
|
|
176
|
+
// - loopback base on the local edge port (:3000): plain-http loopback port
|
|
177
|
+
// swap (BRAIN2-AUDIT: compose services are plain HTTP);
|
|
178
|
+
// - private-LAN base whose host is one of THIS box's addresses: the
|
|
179
|
+
// loopback bind (#572/#623: services bind 127.0.0.1, only :3000 is on LAN).
|
|
180
|
+
// Everything else (another LAN host, a tunnel on a loopback port, a public
|
|
181
|
+
// host) goes through that base's edge at the service's nginx path. The old
|
|
182
|
+
// rule rewrote ANY RFC1918 base to 127.0.0.1:<port>, so `quox -p vm9110 login`
|
|
183
|
+
// against https://192.0.2.68:3000 was sent to this box's own auth service
|
|
184
|
+
// and got 401 invalid_credentials, and a tunnel profile hit local ports.
|
|
185
|
+
//
|
|
186
|
+
// DEVSUITE-GRIPE-P7 / Bug A: computed from the profile url, not getBaseUrl(),
|
|
187
|
+
// so a stale QUOX_URL cannot defeat -p.
|
|
188
|
+
export function deriveServiceUrl(baseUrl, port, service, opts = {}) {
|
|
113
189
|
try {
|
|
114
190
|
const url = new URL(baseUrl);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
191
|
+
const id = localIdentity(opts);
|
|
192
|
+
const basePort = url.port || (url.protocol === 'https:' ? '443' : '80');
|
|
193
|
+
if (LOOPBACK_HOSTS.has(url.hostname) && LOCAL_EDGE_PORTS.has(basePort)) {
|
|
194
|
+
url.port = String(port);
|
|
119
195
|
url.protocol = 'http:';
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
196
|
+
return url.origin;
|
|
197
|
+
}
|
|
198
|
+
if (!LOOPBACK_HOSTS.has(url.hostname) && isPrivateHost(url.hostname) && isThisMachine(url.hostname, id)) {
|
|
199
|
+
url.port = String(port);
|
|
123
200
|
url.protocol = 'http:';
|
|
124
201
|
url.hostname = '127.0.0.1';
|
|
202
|
+
return url.origin;
|
|
203
|
+
}
|
|
204
|
+
if (service && Object.prototype.hasOwnProperty.call(EDGE_PATHS, service)) {
|
|
205
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, '')}${EDGE_PATHS[service]}`;
|
|
125
206
|
}
|
|
207
|
+
// A port-mapped service with no edge route: the old port swap is the only guess left.
|
|
208
|
+
url.port = String(port);
|
|
126
209
|
return url.origin;
|
|
127
210
|
} catch {
|
|
128
211
|
return baseUrl;
|
|
@@ -227,6 +310,9 @@ export function loadConfig(profileOverride) {
|
|
|
227
310
|
// The profile this invocation is using (persisted default, or the -p /
|
|
228
311
|
// QUOX_PROFILE override for this process only). Never written to _data.
|
|
229
312
|
_effectiveProfile: effectiveProfile,
|
|
313
|
+
// quox#805: a profile passed explicitly to loadConfig(profile) means "act as
|
|
314
|
+
// that profile's user", same as -p on the CLI (no implicit attach default).
|
|
315
|
+
_explicitProfile: Boolean(profileOverride),
|
|
230
316
|
|
|
231
317
|
getActiveProfile() {
|
|
232
318
|
const name = this._effectiveProfile;
|
|
@@ -325,6 +411,38 @@ export function loadConfig(profileOverride) {
|
|
|
325
411
|
this.save();
|
|
326
412
|
},
|
|
327
413
|
|
|
414
|
+
// Stale-sign-in fix: another window or `quox login` may have written fresher credentials for this
|
|
415
|
+
// profile since this process loaded. Re-read ONLY the active profile's auth from disk and adopt it in
|
|
416
|
+
// place (callers hold profile refs). Returns true when the auth changed. Never logs or returns a token.
|
|
417
|
+
reloadAuth() {
|
|
418
|
+
try {
|
|
419
|
+
const name = this._effectiveProfile;
|
|
420
|
+
const disk = JSON.parse(readFileSync(getConfigFile(), 'utf-8'));
|
|
421
|
+
const dAuth = disk?.profiles?.[name]?.auth ?? null;
|
|
422
|
+
const prof = this._data.profiles?.[name];
|
|
423
|
+
if (!prof || dAuth == null) return false;
|
|
424
|
+
if (J(prof.auth) === J(dAuth)) return false;
|
|
425
|
+
prof.auth = dAuth;
|
|
426
|
+
// The loaded snapshot now agrees with disk for auth, so save() does not treat it as our change.
|
|
427
|
+
try {
|
|
428
|
+
const snap = JSON.parse(this._loadedProfiles[name] || '{}');
|
|
429
|
+
snap.auth = dAuth;
|
|
430
|
+
this._loadedProfiles[name] = J(snap);
|
|
431
|
+
} catch { /* snapshot is advisory */ }
|
|
432
|
+
this._clearedAuth.delete(name);
|
|
433
|
+
this._refreshedAuth.delete(name);
|
|
434
|
+
return true;
|
|
435
|
+
} catch { return false; }
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
/** mtime (ms) of the credentials file, or 0. A cheap stat for "did anything sign in elsewhere". */
|
|
439
|
+
credentialsMtime() {
|
|
440
|
+
try { return statSync(getConfigFile()).mtimeMs; } catch { return 0; }
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
/** Cross-process refresh lock file, next to the credentials. */
|
|
444
|
+
refreshLockFile() { return `${getConfigFile()}.refresh.lock`; },
|
|
445
|
+
|
|
328
446
|
setAuth(authData, opts = {}) {
|
|
329
447
|
const profile = this.getActiveProfile();
|
|
330
448
|
if (!profile) throw new Error('No active profile.');
|
|
@@ -400,7 +518,7 @@ export function loadConfig(profileOverride) {
|
|
|
400
518
|
// and the shared services config (step 2) still say exactly what they
|
|
401
519
|
// say, and a remote/non-loopback host keeps its scheme (a real
|
|
402
520
|
// deployment may terminate TLS per port).
|
|
403
|
-
return deriveServiceUrl(profileUrl, port);
|
|
521
|
+
return deriveServiceUrl(profileUrl, port, service);
|
|
404
522
|
},
|
|
405
523
|
|
|
406
524
|
get(key) {
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CUSTOM-HATS (owner 2026-10-17): pure helpers that build and edit a custom hat definition.
|
|
3
|
+
* Shared by `quox hat create|edit` (cli/lib/commands/hat.js) and the quox window's /hat new|edit
|
|
4
|
+
* (cli/lib/quoxon). No I/O, no chalk: safe to import anywhere.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The two tools an orchestrator needs to reach the agents a hat lists. */
|
|
8
|
+
export const DELEGATION_TOOLS = ['delegate_to_agent', 'agent_directory'];
|
|
9
|
+
export const HAT_MODES = ['permissive', 'restrictive'];
|
|
10
|
+
export const HAT_OFFERS = ['members', 'authorized'];
|
|
11
|
+
const HARD_CAP = 128;
|
|
12
|
+
|
|
13
|
+
/** "a, b,,c" -> ['a','b','c'] (trimmed, lowercased, deduped, order kept). */
|
|
14
|
+
export function parseList(v) {
|
|
15
|
+
if (v === undefined || v === null || v === true) return [];
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const raw of String(v).split(',')) {
|
|
18
|
+
const x = raw.trim().toLowerCase();
|
|
19
|
+
if (x && !out.includes(x)) out.push(x);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Every tool name a hat names: explicit, bundles, on-demand reads. */
|
|
25
|
+
export function memberTools(def) {
|
|
26
|
+
const t = def?.tools || {};
|
|
27
|
+
const names = [
|
|
28
|
+
...(Array.isArray(t.explicit) ? t.explicit : []),
|
|
29
|
+
...(Array.isArray(t.bundles) ? t.bundles.flatMap(b => (Array.isArray(b?.tools) ? b.tools : [])) : []),
|
|
30
|
+
...(Array.isArray(t.on_demand_reads) ? t.on_demand_reads : []),
|
|
31
|
+
];
|
|
32
|
+
return [...new Set(names)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function emptyDefinition() {
|
|
36
|
+
return {
|
|
37
|
+
schema_version: 1,
|
|
38
|
+
purpose: '',
|
|
39
|
+
agents: { orchestrator: 'quox', specialists: [] },
|
|
40
|
+
tools: { explicit: [], bundles: [], connector_families: [] },
|
|
41
|
+
budgets: { initial_tool_target: 12, max_manifest: 32 },
|
|
42
|
+
transitions: { allowed: 'any', confirm: 'none', privileged: false },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureDelegationTools(def) {
|
|
47
|
+
const specialists = def.agents?.specialists || [];
|
|
48
|
+
if (!specialists.length) return [];
|
|
49
|
+
const explicit = def.tools.explicit;
|
|
50
|
+
const added = DELEGATION_TOOLS.filter(t => !explicit.includes(t));
|
|
51
|
+
explicit.unshift(...added);
|
|
52
|
+
return added;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fitBudgets(def) {
|
|
56
|
+
const b = def.budgets || (def.budgets = {});
|
|
57
|
+
const need = (def.tools.explicit || []).length;
|
|
58
|
+
if (!Number.isInteger(b.max_manifest) || b.max_manifest < need) b.max_manifest = Math.min(HARD_CAP, Math.max(32, need));
|
|
59
|
+
if (!Number.isInteger(b.initial_tool_target) || b.initial_tool_target < 1) b.initial_tool_target = Math.min(12, b.max_manifest);
|
|
60
|
+
if (b.initial_tool_target > b.max_manifest) b.initial_tool_target = b.max_manifest;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Restrictive = the hat AUTHORIZES only its members (+ the delegation pair); this binds every agent
|
|
65
|
+
* working under the hat, the hat's own agents included when delegated to. Permissive = the agent's
|
|
66
|
+
* own grant is the ceiling (the default). A deny list carried from --from is kept either way.
|
|
67
|
+
*/
|
|
68
|
+
export function applyMode(def, mode) {
|
|
69
|
+
if (mode === undefined || mode === null) return;
|
|
70
|
+
if (!HAT_MODES.includes(mode)) throw new Error(`--mode must be ${HAT_MODES.join(' or ')} (got '${mode}')`);
|
|
71
|
+
const deny = Array.isArray(def.authz?.deny) ? def.authz.deny : null;
|
|
72
|
+
if (mode === 'permissive') {
|
|
73
|
+
if (deny) def.authz = { mode: 'permissive', deny };
|
|
74
|
+
else delete def.authz;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
def.authz = { mode: 'restrictive', allow: [...new Set([...memberTools(def), ...DELEGATION_TOOLS])], ...(deny ? { deny } : {}) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The definition a new custom hat starts from: a copy of --from (or an empty hat), with
|
|
82
|
+
* --agents / --tools REPLACING the lists, the delegation tools added when the hat lists agents,
|
|
83
|
+
* the offer set (default 'members': a custom hat offers its orchestrator only what it lists),
|
|
84
|
+
* and budgets widened to fit. Pure: never mutates base.
|
|
85
|
+
*/
|
|
86
|
+
export function buildCustomDefinition({ base = null, name = '', purpose, agents, tools, mode, offer } = {}) {
|
|
87
|
+
const def = base ? JSON.parse(JSON.stringify(base)) : emptyDefinition();
|
|
88
|
+
def.schema_version = 1;
|
|
89
|
+
def.agents = { orchestrator: 'quox', ...(def.agents || {}) };
|
|
90
|
+
if (!Array.isArray(def.agents.specialists)) def.agents.specialists = [];
|
|
91
|
+
def.tools = { ...(def.tools || {}) };
|
|
92
|
+
def.tools.explicit = Array.isArray(def.tools.explicit) ? [...def.tools.explicit] : [];
|
|
93
|
+
if (agents !== undefined) def.agents.specialists = [...agents];
|
|
94
|
+
if (tools !== undefined) def.tools.explicit = [...tools];
|
|
95
|
+
const autoAdded = ensureDelegationTools(def);
|
|
96
|
+
const o = offer === undefined ? 'members' : offer;
|
|
97
|
+
if (!HAT_OFFERS.includes(o)) throw new Error(`--offer must be ${HAT_OFFERS.join(' or ')} (got '${o}')`);
|
|
98
|
+
def.tools.offer = o;
|
|
99
|
+
if (purpose !== undefined && String(purpose).trim()) def.purpose = String(purpose).trim();
|
|
100
|
+
if (!def.purpose || base) {
|
|
101
|
+
if (purpose === undefined || !String(purpose).trim()) def.purpose = `${name || 'Custom'} work${def.agents.specialists.length ? ` with ${def.agents.specialists.join(', ')}` : ''}`;
|
|
102
|
+
}
|
|
103
|
+
fitBudgets(def);
|
|
104
|
+
applyMode(def, mode);
|
|
105
|
+
return { definition: def, autoAdded };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Apply `quox hat edit` options to a copy of a definition. Returns the new definition and a
|
|
110
|
+
* human list of changes (empty = nothing to do). --agents/--tools replace; --add-* / --remove-*
|
|
111
|
+
* adjust. A restrictive allow list follows tool adds/removes so the hat keeps authorizing exactly
|
|
112
|
+
* its members.
|
|
113
|
+
*/
|
|
114
|
+
export function applyHatEdits(current, opts = {}) {
|
|
115
|
+
const def = JSON.parse(JSON.stringify(current || {}));
|
|
116
|
+
def.agents = { orchestrator: 'quox', ...(def.agents || {}) };
|
|
117
|
+
def.tools = { ...(def.tools || {}) };
|
|
118
|
+
let specialists = Array.isArray(def.agents.specialists) ? [...def.agents.specialists] : [];
|
|
119
|
+
let explicit = Array.isArray(def.tools.explicit) ? [...def.tools.explicit] : [];
|
|
120
|
+
const changes = [];
|
|
121
|
+
const beforeAgents = [...specialists];
|
|
122
|
+
const beforeTools = new Set(memberTools(def));
|
|
123
|
+
|
|
124
|
+
if (opts.agents !== undefined) specialists = [...opts.agents];
|
|
125
|
+
for (const a of opts.addAgents || []) if (!specialists.includes(a)) specialists.push(a);
|
|
126
|
+
if ((opts.removeAgents || []).length) specialists = specialists.filter(a => !opts.removeAgents.includes(a));
|
|
127
|
+
if (opts.tools !== undefined) explicit = [...opts.tools];
|
|
128
|
+
for (const t of opts.addTools || []) if (!explicit.includes(t)) explicit.push(t);
|
|
129
|
+
if ((opts.removeTools || []).length) {
|
|
130
|
+
explicit = explicit.filter(t => !opts.removeTools.includes(t));
|
|
131
|
+
if (Array.isArray(def.tools.bundles)) def.tools.bundles = def.tools.bundles.map(b => ({ ...b, tools: (b.tools || []).filter(t => !opts.removeTools.includes(t)) }));
|
|
132
|
+
}
|
|
133
|
+
def.agents.specialists = specialists;
|
|
134
|
+
def.tools.explicit = explicit;
|
|
135
|
+
const autoAdded = (opts.removeTools || []).some(t => DELEGATION_TOOLS.includes(t)) ? [] : ensureDelegationTools(def);
|
|
136
|
+
|
|
137
|
+
const agentsAdded = specialists.filter(a => !beforeAgents.includes(a));
|
|
138
|
+
const agentsRemoved = beforeAgents.filter(a => !specialists.includes(a));
|
|
139
|
+
if (agentsAdded.length) changes.push(`agents +${agentsAdded.join(', +')}`);
|
|
140
|
+
if (agentsRemoved.length) changes.push(`agents -${agentsRemoved.join(', -')}`);
|
|
141
|
+
const afterTools = new Set(memberTools(def));
|
|
142
|
+
const toolsAdded = [...afterTools].filter(t => !beforeTools.has(t));
|
|
143
|
+
const toolsRemoved = [...beforeTools].filter(t => !afterTools.has(t));
|
|
144
|
+
if (toolsAdded.length) changes.push(`tools +${toolsAdded.join(', +')}`);
|
|
145
|
+
if (toolsRemoved.length) changes.push(`tools -${toolsRemoved.join(', -')}`);
|
|
146
|
+
|
|
147
|
+
if (opts.purpose !== undefined && String(opts.purpose).trim() && String(opts.purpose).trim() !== def.purpose) {
|
|
148
|
+
def.purpose = String(opts.purpose).trim();
|
|
149
|
+
changes.push('purpose');
|
|
150
|
+
}
|
|
151
|
+
if (opts.offer !== undefined) {
|
|
152
|
+
if (!HAT_OFFERS.includes(opts.offer)) throw new Error(`--offer must be ${HAT_OFFERS.join(' or ')} (got '${opts.offer}')`);
|
|
153
|
+
if ((def.tools.offer || 'authorized') !== opts.offer) { def.tools.offer = opts.offer; changes.push(`offer ${opts.offer}`); }
|
|
154
|
+
}
|
|
155
|
+
const prevMode = def.authz?.mode === 'restrictive' ? 'restrictive' : 'permissive';
|
|
156
|
+
if (opts.mode !== undefined) {
|
|
157
|
+
applyMode(def, opts.mode);
|
|
158
|
+
if (opts.mode !== prevMode) changes.push(`mode ${opts.mode}`);
|
|
159
|
+
} else if (prevMode === 'restrictive' && Array.isArray(def.authz?.allow) && (toolsAdded.length || toolsRemoved.length)) {
|
|
160
|
+
const allow = new Set(def.authz.allow);
|
|
161
|
+
for (const t of toolsAdded) allow.add(t);
|
|
162
|
+
for (const t of toolsRemoved) allow.delete(t);
|
|
163
|
+
def.authz = { ...def.authz, allow: [...allow] };
|
|
164
|
+
}
|
|
165
|
+
fitBudgets(def);
|
|
166
|
+
return { definition: def, changes, autoAdded };
|
|
167
|
+
}
|
|
168
|
+
|
|
@@ -145,6 +145,14 @@ export async function fetchEngine({ dest, needsSudo, force, client, execFn, fsOp
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
const res = await client.get('/download/quoxgauge-bundle.tar.gz', { service: 'collector', raw: true });
|
|
148
|
+
// #988: a public install's collector has no DoneEngine bundle (dev servers build it from the private quox-gauge
|
|
149
|
+
// repo), so a 404 is a fact about this install, not a transient failure worth retrying.
|
|
150
|
+
if (res && res.status === 404) {
|
|
151
|
+
const err = new Error('DoneEngine is not part of this Quox install: this collector does not carry the DoneEngine bundle '
|
|
152
|
+
+ '(it is delivered to Quox dev servers by `quox dev enable`). Nothing was changed.');
|
|
153
|
+
err.code = 'DONEENGINE_NOT_SHIPPED';
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
148
156
|
if (!res || !res.ok) {
|
|
149
157
|
throw new Error(`Bundle download failed (status ${res ? res.status : 'no response'}) from /download/quoxgauge-bundle.tar.gz`);
|
|
150
158
|
}
|