@bitkyc08/opencodex 2.50.0 → 2.52.0-preview.20260911
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/bin/ocx.mjs +222 -71
- package/gui/dist/assets/{index-C39tnjXO.js → index-Dx0xv2EA.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- package/src/cli/capabilities.ts +28 -0
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +94 -30
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/codex/account-usability.ts +48 -12
- package/src/codex/auth-api.ts +49 -5
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/sync.ts +85 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +26 -10
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +126 -12
- package/src/generated/compatibility-version.json +136 -76
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry.ts +34 -5
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -3
- package/src/server/claude-messages.ts +7 -1
- package/src/server/hub-state.ts +98 -0
- package/src/server/index.ts +124 -6
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +75 -4
- package/src/server/responses/compact.ts +20 -9
- package/src/server/responses/core.ts +57 -10
- package/src/server/responses/policy-fallback.ts +7 -1
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +906 -94
- package/src/types/config.ts +57 -4
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
package/src/cli/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
pendingTeardownPathFor,
|
|
49
49
|
quarantinePendingTeardown,
|
|
50
50
|
} from "../config/pending-teardown";
|
|
51
|
-
import { collectStatus, unusedProxyWarningLines } from "./status";
|
|
51
|
+
import { collectStatus, hubStatusLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
|
|
52
52
|
import { endpointsToProve, everyEndpointProvenDown, sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
|
|
53
53
|
import { takeFlag } from "./runtime-api";
|
|
54
54
|
|
|
@@ -66,10 +66,11 @@ import { dispatchCommand , decideStartWithLiveOwner } from "./dispatch";
|
|
|
66
66
|
import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
67
67
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
68
68
|
import { createReadinessGate } from "../server/readiness";
|
|
69
|
+
import { isApiAuthRequired } from "../server/auth-cors";
|
|
69
70
|
import { runReady, type ReadyArgs } from "./ready";
|
|
70
71
|
import { runCli } from "./root";
|
|
71
|
-
import { isProcessAlive, ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
|
|
72
|
-
import {
|
|
72
|
+
import { isProcessAlive, ProxyOwnershipRefusedError, refusalNextStep, stopProxy } from "../lib/process-control";
|
|
73
|
+
import { startupDataPlaneToken } from "../lib/service-secrets";
|
|
73
74
|
import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, proxyStillLiveAfterStop, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalledDetailed, uninstallServiceIfInstalled, uninstallServiceDetailed } from "../service";
|
|
74
75
|
import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
75
76
|
import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
|
|
@@ -81,8 +82,11 @@ import { scheduleCatalogPrewarm } from "./catalog-prewarm";
|
|
|
81
82
|
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
82
83
|
import { syncModelsToCodex } from "../codex/sync";
|
|
83
84
|
import {
|
|
85
|
+
HUB_GATED_SKIP_MESSAGE,
|
|
86
|
+
localClientSkipReason,
|
|
84
87
|
shouldSyncGrokOnStart,
|
|
85
88
|
syncCodexOnStartIfEnabled,
|
|
89
|
+
type LocalClientSkipReason,
|
|
86
90
|
} from "../codex/desired-state";
|
|
87
91
|
import {
|
|
88
92
|
reconcileClientStartupBeforeReady,
|
|
@@ -174,6 +178,19 @@ async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
|
174
178
|
return null;
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The one startup line for "nothing was written to Codex".
|
|
183
|
+
*
|
|
184
|
+
* Two very different facts reached it: the user's own OFF switch, and a hub declining to
|
|
185
|
+
* rewrite its own local clients. Printing the toggle's wording for the gate is what made
|
|
186
|
+
* operators hunt for a switch they never set (#4236).
|
|
187
|
+
*/
|
|
188
|
+
function startupLeftCodexNativeLine(reason: LocalClientSkipReason): string {
|
|
189
|
+
return reason === "hub-gated"
|
|
190
|
+
? ` ${HUB_GATED_SKIP_MESSAGE} Startup left Codex native.`
|
|
191
|
+
: " Codex integration OFF; startup left Codex native.";
|
|
192
|
+
}
|
|
193
|
+
|
|
177
194
|
/** Argv for detached `start`, optionally hard-pinning the listen port. */
|
|
178
195
|
function startArgv(port?: number): string[] {
|
|
179
196
|
const args = ["start"];
|
|
@@ -190,6 +207,10 @@ async function chooseListenPort(
|
|
|
190
207
|
const config = loadConfig();
|
|
191
208
|
const preferred = requestedPort ?? config.port ?? 10100;
|
|
192
209
|
const hardPin = requestedPort !== undefined && requestedPort > 0;
|
|
210
|
+
// Only an EXPLICITLY ported listener reserves a port. The companion form (`{enabled:true}`
|
|
211
|
+
// with no port) deliberately shares the public port on 127.0.0.1, so treating it as a
|
|
212
|
+
// reservation — via `effectiveLoopbackListenerPort` — would refuse every start on a
|
|
213
|
+
// one-port hub and hop the public listener onto an ephemeral port (#4236).
|
|
193
214
|
const reservedLoopbackPort = config.unauthenticatedLoopbackListener?.enabled
|
|
194
215
|
? config.unauthenticatedLoopbackListener.port
|
|
195
216
|
: undefined;
|
|
@@ -271,10 +292,16 @@ async function findProxyOwnerBeforeJournalRecovery(
|
|
|
271
292
|
}
|
|
272
293
|
|
|
273
294
|
async function handleStart(options: { block?: boolean } = {}) {
|
|
274
|
-
// Native (WinSW) service mode has no batch wrapper to read the service token file
|
|
275
|
-
//
|
|
295
|
+
// Native (WinSW) service mode has no batch wrapper to read the service token file into
|
|
296
|
+
// the environment, and a FOREGROUND `ocx start` has no wrapper at all — so the app loads
|
|
297
|
+
// the token here, before the server binds, with the same precedence the launchd plist and
|
|
298
|
+
// the systemd unit use when they cat the file into the environment. Without the second
|
|
299
|
+
// source, `ocx start` refused to bind a non-loopback hostname (assertServerAuthConfig)
|
|
300
|
+
// that the installed service on the same machine was serving happily (#4236). The server
|
|
276
301
|
// auth path reads OPENCODEX_API_AUTH_TOKEN from the environment.
|
|
277
|
-
const serviceToken =
|
|
302
|
+
const serviceToken = startupDataPlaneToken(process.env, {
|
|
303
|
+
authRequired: isApiAuthRequired(loadConfig()),
|
|
304
|
+
});
|
|
278
305
|
if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
|
|
279
306
|
// The service wrapper (and WinSW via OCX_API_TOKEN_FILE) can still export a colliding
|
|
280
307
|
// token that install now refuses to write. Refuse it here too, before bind, so an
|
|
@@ -513,7 +540,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
513
540
|
}
|
|
514
541
|
},
|
|
515
542
|
);
|
|
516
|
-
if (!startupSync.ran) console.log(
|
|
543
|
+
if (!startupSync.ran) console.log(startupLeftCodexNativeLine(localClientSkipReason(config)));
|
|
517
544
|
await refreshOwnedRaycastCatalog(config, port);
|
|
518
545
|
// #1046: one warning per startup, after BOTH writes. The server's cache
|
|
519
546
|
// invalidation happens first and the catalog sync second, so the mtime is only
|
|
@@ -579,7 +606,9 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
579
606
|
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
580
607
|
return null;
|
|
581
608
|
});
|
|
582
|
-
if (synced?.status === "skipped")
|
|
609
|
+
if (synced?.status === "skipped") {
|
|
610
|
+
console.log(startupLeftCodexNativeLine(synced.skippedReason ?? "desired_disabled"));
|
|
611
|
+
}
|
|
583
612
|
// Do not refresh Raycast from saved config here: live bind/admission and
|
|
584
613
|
// secondary-listener settings may differ. Explicit sync or server startup
|
|
585
614
|
// owns catalog refresh; ensure must not overwrite a working destination.
|
|
@@ -626,7 +655,9 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
626
655
|
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
627
656
|
return null;
|
|
628
657
|
});
|
|
629
|
-
if (synced?.status === "skipped")
|
|
658
|
+
if (synced?.status === "skipped") {
|
|
659
|
+
console.log(startupLeftCodexNativeLine(synced.skippedReason ?? "desired_disabled"));
|
|
660
|
+
}
|
|
630
661
|
// The child performs Raycast refresh with its actual startup config. The
|
|
631
662
|
// parent's pre-spawn snapshot is not authoritative for a client-file write.
|
|
632
663
|
// The child opens /healthz before its best-effort roster reconcile. Await the same idempotent
|
|
@@ -952,7 +983,12 @@ async function handleStop() {
|
|
|
952
983
|
if (detail) console.error(` ${detail}`);
|
|
953
984
|
if (err instanceof ProxyOwnershipRefusedError) {
|
|
954
985
|
ownershipBlocked = true;
|
|
955
|
-
|
|
986
|
+
// Every refusal `POST /api/stop` produces is written for an API client, so it
|
|
987
|
+
// recommends `ocx stop` — the command printing it. Following that advice returns
|
|
988
|
+
// the operator to this exact message, which is the loop #4169 was filed for. The
|
|
989
|
+
// service manager was already asked to stop above, so name what is actually left.
|
|
990
|
+
console.error(` ${refusalNextStep(err.code)}`);
|
|
991
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config): the refusing proxy is still running.");
|
|
956
992
|
}
|
|
957
993
|
}
|
|
958
994
|
} else {
|
|
@@ -979,7 +1015,9 @@ async function handleStop() {
|
|
|
979
1015
|
if (detail) console.error(` ${detail}`);
|
|
980
1016
|
if (err instanceof ProxyOwnershipRefusedError) {
|
|
981
1017
|
ownershipBlocked = true;
|
|
982
|
-
|
|
1018
|
+
// Same loop as the tracked-pid path above: the refusal recommends this command.
|
|
1019
|
+
console.error(` ${refusalNextStep(err.code)}`);
|
|
1020
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config): the refusing proxy is still running.");
|
|
983
1021
|
}
|
|
984
1022
|
}
|
|
985
1023
|
} else if (live) {
|
|
@@ -1345,12 +1383,22 @@ async function handleStatus() {
|
|
|
1345
1383
|
return;
|
|
1346
1384
|
}
|
|
1347
1385
|
|
|
1386
|
+
// First line of the report, above the proxy line, deliberately (#4236): on a connected client
|
|
1387
|
+
// the provider/login/model lines describe the HUB, and the lines that describe this machine
|
|
1388
|
+
// are tagged `(local)`. A reader who sees neither draws the wrong conclusion from a correct
|
|
1389
|
+
// report — an agent read `xai ✗ not logged in` off a client and decided the hub could not
|
|
1390
|
+
// serve grok.
|
|
1391
|
+
const remoteHubBanner = remoteHubBannerLine(status.json.remoteHub);
|
|
1392
|
+
if (remoteHubBanner) console.log(remoteHubBanner);
|
|
1393
|
+
// `(local)` only while connected: on a standalone install every line is local, and tagging
|
|
1394
|
+
// them all would be noise that trains the reader to skip the tag.
|
|
1395
|
+
const local = status.json.remoteHub.connected ? " (local)" : "";
|
|
1348
1396
|
if (status.json.proxy.pid || status.json.proxy.health.ok) {
|
|
1349
|
-
console.log(`✅ Proxy: ${status.proxyLabel}`);
|
|
1397
|
+
console.log(`✅ Proxy: ${status.proxyLabel}${local}`);
|
|
1350
1398
|
} else {
|
|
1351
|
-
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
1399
|
+
console.log(`❌ Proxy: ${status.proxyLabel}${local}`);
|
|
1352
1400
|
}
|
|
1353
|
-
console.log(` Health: ${status.healthLabel}`);
|
|
1401
|
+
console.log(` Health: ${status.healthLabel}${local}`);
|
|
1354
1402
|
if (status.json.claudeDesktop.desiredEnabled && !status.json.claudeDesktop.policy.ok) {
|
|
1355
1403
|
console.log(` ⚠️ Claude Desktop 3P health: ${status.json.claudeDesktop.policy.status}`);
|
|
1356
1404
|
console.log(` ${status.json.claudeDesktop.policy.message}`);
|
|
@@ -1390,25 +1438,31 @@ async function handleStatus() {
|
|
|
1390
1438
|
? " Restart with 'ocx start', or refresh the installed service: 'ocx service repair'."
|
|
1391
1439
|
: " Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
|
|
1392
1440
|
}
|
|
1393
|
-
console.log(` Dashboard: ${status.json.dashboard.url}`);
|
|
1394
|
-
console.log(` Config: ${status.json.paths.config}`);
|
|
1395
|
-
console.log(` PID file: ${status.json.paths.pid}`);
|
|
1396
|
-
console.log(` Runtime: ${status.json.paths.runtime}`);
|
|
1397
|
-
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`);
|
|
1398
|
-
|
|
1441
|
+
console.log(` Dashboard: ${status.json.dashboard.url}${local}`);
|
|
1442
|
+
console.log(` Config: ${status.json.paths.config}${local}`);
|
|
1443
|
+
console.log(` PID file: ${status.json.paths.pid}${local}`);
|
|
1444
|
+
console.log(` Runtime: ${status.json.paths.runtime}${local}`);
|
|
1445
|
+
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}${local}`);
|
|
1446
|
+
// On a client this is the local default, which routing does not use — the hub applies its own.
|
|
1447
|
+
console.log(` Default provider: ${status.json.defaultProvider}${local}`);
|
|
1448
|
+
// One block rather than six scattered lines, and only on a hub: `hubStatusLines` owns the
|
|
1449
|
+
// sentences so they are testable without spawning the CLI. It prints no token value.
|
|
1450
|
+
if (status.json.hub) {
|
|
1451
|
+
for (const line of hubStatusLines(status.json.hub)) console.log(` ${line}`);
|
|
1452
|
+
}
|
|
1399
1453
|
console.log(` Remote hub: ${status.json.connection.state}${status.json.connection.serverUrl ? ` (${status.json.connection.serverUrl})` : ""}`);
|
|
1400
1454
|
if (status.json.connection.state === "invalid" || status.json.connection.state === "mismatched") {
|
|
1401
1455
|
console.log(` ⚠️ ${status.json.connection.reason}`);
|
|
1402
1456
|
}
|
|
1403
|
-
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
1404
|
-
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
|
|
1405
|
-
console.log(` ${formatStartupRoutingDetail(status.json.startup)}`);
|
|
1406
|
-
console.log(` Service: ${status.json.service.summary}`);
|
|
1407
|
-
console.log(` ${status.json.codexShim.summary}`);
|
|
1408
|
-
console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
|
|
1409
|
-
console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}`);
|
|
1410
|
-
console.log(` Codex source: ${status.json.codexRuntime.source}`);
|
|
1411
|
-
console.log(` Codex home: ${status.json.codexHome.effectiveCodexHome}`);
|
|
1457
|
+
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}${local}`);
|
|
1458
|
+
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}${local}`);
|
|
1459
|
+
console.log(` ${formatStartupRoutingDetail(status.json.startup)}${local}`);
|
|
1460
|
+
console.log(` Service: ${status.json.service.summary}${local}`);
|
|
1461
|
+
console.log(` ${status.json.codexShim.summary}${local}`);
|
|
1462
|
+
console.log(` Codex runtime: ${status.json.codexRuntime.path}${local}`);
|
|
1463
|
+
console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}${local}`);
|
|
1464
|
+
console.log(` Codex source: ${status.json.codexRuntime.source}${local}`);
|
|
1465
|
+
console.log(` Codex home: ${status.json.codexHome.effectiveCodexHome}${local}`);
|
|
1412
1466
|
if (status.json.codexHome.warning) {
|
|
1413
1467
|
console.log(` ⚠️ ${status.json.codexHome.warning}`);
|
|
1414
1468
|
console.log(` Action: ${status.json.codexHome.action}`);
|
|
@@ -1430,7 +1484,17 @@ async function handleStatus() {
|
|
|
1430
1484
|
const { collectOAuthHealthEntriesForCli, oauthLoginSummary } = await import("../oauth");
|
|
1431
1485
|
const { emailMaskingEnabled } = await import("../lib/privacy");
|
|
1432
1486
|
const { formatOAuthHealthForStatus } = await import("./status-oauth");
|
|
1433
|
-
|
|
1487
|
+
// On a connected client the HUB's providers and logins come first, because they are the ones
|
|
1488
|
+
// that decide what a request can route to. The local block still prints — an operator debugging
|
|
1489
|
+
// a half-migrated machine needs to see it — but under a heading that says it is not in use, and
|
|
1490
|
+
// below the hub's, so the hub's is what a reader (or an agent) encounters first (#4236).
|
|
1491
|
+
for (const line of remoteHubStatusLines(status.json.remoteHub)) console.log(` ${line}`);
|
|
1492
|
+
if (status.json.remoteHub.connected) {
|
|
1493
|
+
console.log(status.json.remoteHub.stateSource === "unavailable"
|
|
1494
|
+
? " Local-only credential state (this is NOT the hub's; the hub's state could not be read):"
|
|
1495
|
+
: " Local-only (not used for routing while connected):");
|
|
1496
|
+
}
|
|
1497
|
+
console.log(` OAuth logins${local}:`);
|
|
1434
1498
|
// The operator's own `privacy.maskEmails` decision applies to the CLI too: `ocx status` is not
|
|
1435
1499
|
// the dashboard, but it reads the same stored addresses, and a flag that only moved one of the
|
|
1436
1500
|
// two would leave the operator unable to tell which surface they had configured.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Trusted facts captured by the plain-Node
|
|
2
|
+
* Trusted facts captured by the plain-Node package launcher before Bun auto-loads
|
|
3
3
|
* project dotenv files. The random proof travels in argv while the context
|
|
4
4
|
* travels in the environment, so a project `.env` cannot forge the pair during
|
|
5
5
|
* an ordinary `ocx ...` invocation.
|
package/src/cli/registry.ts
CHANGED
|
@@ -64,9 +64,17 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
64
64
|
usage: "ocx service [install|repair|restart|start|stop|status|uninstall|remove]",
|
|
65
65
|
summary: "Run as a background service.",
|
|
66
66
|
details: [
|
|
67
|
-
"With no subcommand, installs when absent or repairs
|
|
68
|
-
"`
|
|
69
|
-
"
|
|
67
|
+
"With no subcommand, installs when absent or repairs an existing service.",
|
|
68
|
+
"`repair` refreshes the definition and reloads the manager only when something changed, so repairing a healthy service is not an outage.",
|
|
69
|
+
"`restart` is the same refresh but always restarts: on macOS an unchanged, already-loaded job is kickstarted in place. Healthy Windows tasks are reused, while stale definitions may re-register and elevate.",
|
|
70
|
+
"Use `ocx service status` to see diagnostics and log paths. On macOS it reports four states:",
|
|
71
|
+
"loaded from the current plist, loaded from an OLDER plist (repair), not loaded (repair), or",
|
|
72
|
+
"`launchd state could not be verified` -- which is an unanswerable probe, NOT a down service.",
|
|
73
|
+
"Data-plane token: nothing has to be exported by hand. On a non-loopback hostname install/repair uses",
|
|
74
|
+
"OPENCODEX_API_AUTH_TOKEN when set, otherwise reuses the existing owner-only ~/.opencodex/service-api-token,",
|
|
75
|
+
"otherwise generates one; the launch wrapper reads that file at start and the value never enters a plist,",
|
|
76
|
+
"a unit file, or argv. An ADMIN token in OPENCODEX_API_AUTH_TOKEN is refused -- unset it and rerun.",
|
|
77
|
+
"Repair and restart never ask for the environment variable again once the token file exists.",
|
|
70
78
|
],
|
|
71
79
|
},
|
|
72
80
|
{
|
|
@@ -155,6 +163,38 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
155
163
|
"The printed grant is secret, single-use, short-lived, and must not be persisted.",
|
|
156
164
|
],
|
|
157
165
|
},
|
|
166
|
+
{
|
|
167
|
+
name: "hub",
|
|
168
|
+
usage: "ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]",
|
|
169
|
+
summary: "Hub-side commands. `invite` prints a ready-to-run `ocx connect` line for one more machine.",
|
|
170
|
+
details: [
|
|
171
|
+
"Topology: a hub serves ONE port. Remote machines dial `hostname:port` with their own per-client",
|
|
172
|
+
"key; the hub's own local processes dial `127.0.0.1:<same port>` with no credential, through the",
|
|
173
|
+
"loopback companion listener enabled by `unauthenticatedLoopbackListener: {\"enabled\": true}`.",
|
|
174
|
+
"The browser-facing management plane is separate: a loopback-only ingress published by an",
|
|
175
|
+
"operator-owned HTTPS frontend (Tailscale Serve) and advertised as hub.managementPublicOrigin.",
|
|
176
|
+
"",
|
|
177
|
+
"Nothing needs to be exported by hand. `ocx service install` provisions an owner-only data-plane",
|
|
178
|
+
"token at ~/.opencodex/service-api-token and the launch wrapper reads it at start; the value never",
|
|
179
|
+
"enters a plist, a unit file, argv, or the environment you typed in. Never copy that file to another",
|
|
180
|
+
"machine -- every client gets its own revocable key from the pairing exchange.",
|
|
181
|
+
"",
|
|
182
|
+
"invite requires a running hub and mints a single-use, short-lived pairing code through the same",
|
|
183
|
+
"attested local route `ocx gui pair` uses, then prints the exact command to run on the other",
|
|
184
|
+
"machine. The code is bound to hub.managementPublicOrigin and to the connecting machine's local",
|
|
185
|
+
"browser origin (`http://localhost:10100` unless corsAllowOrigins names another loopback origin);",
|
|
186
|
+
"the bound origin is always printed, and a different one names the port the client needs.",
|
|
187
|
+
"--data-url overrides the advertised data origin; hub.dataPublicOrigin is the persistent form, and",
|
|
188
|
+
"the fallback is http://<bind address>:<port>. A loopback or wildcard bind has no such address, so",
|
|
189
|
+
"invite refuses instead of advertising http://localhost:<port>, which would tell the other machine",
|
|
190
|
+
"to dial itself and spend the code.",
|
|
191
|
+
"--management-url is a CONFIRMATION, not an override: the grant is bound to",
|
|
192
|
+
"hub.managementPublicOrigin, so a differing value is refused instead of printed.",
|
|
193
|
+
"--clients chooses which client configs the printed ocx connect line will point at the hub.",
|
|
194
|
+
"--json emits { code, expiresAt, dataUrl, managementUrl, command }. Do not persist the code.",
|
|
195
|
+
"Hub state, including the data token and the companion listener, is reported by `ocx status`.",
|
|
196
|
+
],
|
|
197
|
+
},
|
|
158
198
|
{
|
|
159
199
|
name: "update",
|
|
160
200
|
usage: "ocx update [--tag latest|preview]",
|