@bitkyc08/opencodex 2.49.0 → 2.51.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/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/index-D7BdZpZm.js +115 -0
- 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/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +43 -1
- 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 +99 -31
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- 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/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +50 -13
- package/src/codex/auth-api.ts +156 -28
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +99 -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 +29 -12
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- 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/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +184 -12
- package/src/generated/compatibility-version.json +188 -116
- 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/errors.ts +8 -0
- 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/privacy.ts +25 -0
- 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/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +20 -1
- package/src/providers/registry.ts +35 -6
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +10 -7
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +12 -6
- package/src/server/hub-state.ts +98 -0
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +149 -8
- 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/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/request-log.ts +10 -0
- 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 +76 -5
- package/src/server/responses/compact.ts +28 -11
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +201 -48
- package/src/server/responses/policy-fallback.ts +13 -3
- package/src/server/search.ts +2 -2
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +965 -68
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +93 -4
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- 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/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
package/src/cli/connect.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { existsSync, lstatSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
2
2
|
import { DEFAULT_CATALOG_PATH } from "../codex/paths";
|
|
3
|
+
import { codexSupportedReasoningEfforts } from "../codex/catalog/effort";
|
|
4
|
+
import { resolveCodexRuntime } from "../codex/runtime";
|
|
5
|
+
import {
|
|
6
|
+
inspectClientCatalogReadiness,
|
|
7
|
+
type CatalogCompatibilityDeps,
|
|
8
|
+
type ClientCatalogFileState,
|
|
9
|
+
type ClientCatalogReadiness,
|
|
10
|
+
} from "../client/catalog-compatibility";
|
|
3
11
|
import {
|
|
4
12
|
disconnectClient,
|
|
5
13
|
revokeConnectedClientKey,
|
|
@@ -26,6 +34,12 @@ import {
|
|
|
26
34
|
|
|
27
35
|
export interface ClientCommandDeps extends RuntimeApiDeps {
|
|
28
36
|
lifecycleLockDeps?: ClientLifecycleLockDeps;
|
|
37
|
+
catalogProbeDeps?: ClientCatalogProbeDeps;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ClientCatalogProbeDeps extends CatalogCompatibilityDeps {
|
|
41
|
+
/** Injected in tests; defaults to reading the materialized client catalog off disk. */
|
|
42
|
+
readCatalogBody?: () => string | null;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
export const CONNECT_USAGE = `Usage:
|
|
@@ -56,21 +70,96 @@ export type ClientConnectionStatus = {
|
|
|
56
70
|
catalog: "present" | "missing" | "unsafe";
|
|
57
71
|
token: "owned" | "missing" | "changed" | "unsafe";
|
|
58
72
|
rotation: "clean" | "orphan-cleaned" | "recovery-required" | "unsafe";
|
|
73
|
+
/**
|
|
74
|
+
* Whether the selected local Codex CLI can actually launch against this connection (#4207).
|
|
75
|
+
*
|
|
76
|
+
* `state: "connected"` proves the hub answered and the credential works. It never proved the
|
|
77
|
+
* local runtime could consume what was downloaded, which is how a connection kept reporting
|
|
78
|
+
* itself healthy while `codex exec` exited on `unknown variant` before its first request.
|
|
79
|
+
*
|
|
80
|
+
* Reported only while connected, and reported as its own field rather than as a fourth
|
|
81
|
+
* `catalog` value: the status JSON is documented additive-only, so widening an existing
|
|
82
|
+
* field's value domain would change what `catalog: "present"` means for every consumer that
|
|
83
|
+
* already reads it.
|
|
84
|
+
*/
|
|
85
|
+
readiness?: ClientCatalogReadiness["kind"];
|
|
86
|
+
/** Present whenever readiness is not `ready`; names the fault and the way out. */
|
|
87
|
+
readinessReason?: string;
|
|
59
88
|
};
|
|
60
89
|
|
|
61
|
-
|
|
90
|
+
function readInstalledCatalogBody(): string | null {
|
|
91
|
+
try {
|
|
92
|
+
return readFileSync(DEFAULT_CATALOG_PATH, "utf8");
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The ladder the selected Codex CLI accepts, observed without persisting anything.
|
|
100
|
+
*
|
|
101
|
+
* `codexSupportedReasoningEfforts()` with no deps reaches `resolveAndPersistCodexRuntime`, which
|
|
102
|
+
* writes codex-runtime.json. `ocx status` deliberately resolves without persisting, and a
|
|
103
|
+
* read-only diagnostics command should not start writing runtime selection state because a
|
|
104
|
+
* readiness check was added to it. Handing the already-resolved command in as the only candidate
|
|
105
|
+
* skips that path and reuses the resolve cache `ocx status` has usually already filled.
|
|
106
|
+
*/
|
|
107
|
+
function observeLocalCodexEffortLadder(): ReadonlySet<string> | null {
|
|
108
|
+
const command = resolveCodexRuntime().runtime.command;
|
|
109
|
+
return codexSupportedReasoningEfforts({ commandCandidates: () => [command] });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* One observer per command, for the write-time gate and the readiness check alike. Built even
|
|
114
|
+
* when nothing was injected, so production does not silently fall back to the default inside
|
|
115
|
+
* {@link assertClientCatalogCompatible} — that default persists runtime selection state and
|
|
116
|
+
* would run its own probe, which is how the two checks could disagree about the ladder within
|
|
117
|
+
* a single `ocx connect`.
|
|
118
|
+
*/
|
|
119
|
+
function catalogObserver(deps: ClientCatalogProbeDeps | undefined): CatalogCompatibilityDeps {
|
|
120
|
+
return { supportedEfforts: deps?.supportedEfforts ?? observeLocalCodexEffortLadder };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The stat half of the catalog verdict, shared by the status collector and `ocx connect`. */
|
|
124
|
+
function installedCatalogFileState(): ClientCatalogFileState {
|
|
125
|
+
if (!existsSync(DEFAULT_CATALOG_PATH)) return "missing";
|
|
126
|
+
try {
|
|
127
|
+
const stat = lstatSync(DEFAULT_CATALOG_PATH);
|
|
128
|
+
return !stat.isSymbolicLink() && stat.isFile() ? "present" : "unsafe";
|
|
129
|
+
} catch {
|
|
130
|
+
return "unsafe";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Observing the runtime spawns `codex debug models`, so this runs only for a connected client —
|
|
136
|
+
* the one configuration that installs hub bytes the local clamp never touched. A standalone or
|
|
137
|
+
* hub install pays nothing for it.
|
|
138
|
+
*
|
|
139
|
+
* Never throws. A status command that dies because a Codex probe failed would replace one wrong
|
|
140
|
+
* answer with a worse one.
|
|
141
|
+
*/
|
|
142
|
+
function inspectInstalledCatalogReadiness(
|
|
143
|
+
file: ClientCatalogFileState,
|
|
144
|
+
deps: ClientCatalogProbeDeps,
|
|
145
|
+
): ClientCatalogReadiness {
|
|
146
|
+
try {
|
|
147
|
+
const read = deps.readCatalogBody ?? readInstalledCatalogBody;
|
|
148
|
+
return inspectClientCatalogReadiness(file, file === "present" ? read() : null, catalogObserver(deps));
|
|
149
|
+
} catch {
|
|
150
|
+
return { kind: "unverified", reason: "the selected local Codex runtime could not be inspected" };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function collectClientConnectionStatus(
|
|
155
|
+
now = Date.now(),
|
|
156
|
+
lifecycleLockDeps?: ClientLifecycleLockDeps,
|
|
157
|
+
catalogProbeDeps: ClientCatalogProbeDeps = {},
|
|
158
|
+
): ClientConnectionStatus {
|
|
62
159
|
const state = readClientConnectionState();
|
|
63
160
|
const tokenState = readServiceApiTokenState();
|
|
64
161
|
const rotation = inspectClientRotationRecoveryGate(state, lifecycleLockDeps).kind;
|
|
65
|
-
|
|
66
|
-
if (existsSync(DEFAULT_CATALOG_PATH)) {
|
|
67
|
-
try {
|
|
68
|
-
const stat = lstatSync(DEFAULT_CATALOG_PATH);
|
|
69
|
-
catalog = !stat.isSymbolicLink() && stat.isFile() ? "present" : "unsafe";
|
|
70
|
-
} catch {
|
|
71
|
-
catalog = "unsafe";
|
|
72
|
-
}
|
|
73
|
-
}
|
|
162
|
+
const catalog = installedCatalogFileState();
|
|
74
163
|
if (state.kind !== "connected") {
|
|
75
164
|
return {
|
|
76
165
|
state: state.kind,
|
|
@@ -88,6 +177,7 @@ export function collectClientConnectionStatus(now = Date.now(), lifecycleLockDep
|
|
|
88
177
|
: tokenState.kind === "unsafe"
|
|
89
178
|
? "unsafe"
|
|
90
179
|
: tokenState.fingerprint === state.value.tokenFingerprint ? "owned" : "changed";
|
|
180
|
+
const readiness = inspectInstalledCatalogReadiness(catalog, catalogProbeDeps);
|
|
91
181
|
return {
|
|
92
182
|
state: "connected",
|
|
93
183
|
serverUrl: state.value.serverUrl,
|
|
@@ -102,6 +192,8 @@ export function collectClientConnectionStatus(now = Date.now(), lifecycleLockDep
|
|
|
102
192
|
catalog,
|
|
103
193
|
token,
|
|
104
194
|
rotation,
|
|
195
|
+
readiness: readiness.kind,
|
|
196
|
+
...(readiness.kind === "ready" ? {} : { readinessReason: readiness.reason }),
|
|
105
197
|
};
|
|
106
198
|
}
|
|
107
199
|
|
|
@@ -113,12 +205,74 @@ function parseClients(raw: string | undefined): OcxConnectedClientId[] {
|
|
|
113
205
|
return values as OcxConnectedClientId[];
|
|
114
206
|
}
|
|
115
207
|
|
|
208
|
+
/** Reads as a verdict, not a field dump: "ready" is the only word that means the client works. */
|
|
209
|
+
function readinessLine(status: ClientConnectionStatus): string {
|
|
210
|
+
const label = status.readiness === "ready"
|
|
211
|
+
? "ready"
|
|
212
|
+
: status.readiness === "incompatible"
|
|
213
|
+
? "not ready"
|
|
214
|
+
: "unverified";
|
|
215
|
+
return `Local Codex CLI: ${label}${status.readinessReason ? ` (${status.readinessReason})` : ""}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type ConnectCompletionReport = {
|
|
219
|
+
readonly lines: readonly string[];
|
|
220
|
+
/** Non-null when `ocx connect` must exit non-zero rather than report success. */
|
|
221
|
+
readonly failure: string | null;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* What `ocx connect` says once the hub and the credential are settled, and whether the command
|
|
226
|
+
* still fails (#4207).
|
|
227
|
+
*
|
|
228
|
+
* Pure so the fail-closed decision can be exercised without a hub. Two rules it encodes:
|
|
229
|
+
*
|
|
230
|
+
* On a proven incompatibility the verdict is printed FIRST and the `Connected to …` line is
|
|
231
|
+
* withheld, because a caller grepping for that phrase would otherwise read a catalog the local
|
|
232
|
+
* CLI cannot parse as a success. The connection really was saved, so the replacement line says
|
|
233
|
+
* where to see it.
|
|
234
|
+
*
|
|
235
|
+
* And that failure applies only when this connection selected Codex. A Claude-only connection
|
|
236
|
+
* never launches the Codex CLI, so an old binary somewhere on PATH is not a reason to fail the
|
|
237
|
+
* operator's Claude Desktop setup — it is still worth saying, which is why the line survives
|
|
238
|
+
* without the exit code.
|
|
239
|
+
*/
|
|
240
|
+
export function connectCompletionReport(
|
|
241
|
+
connection: { serverUrl: string; apiKeyId: string },
|
|
242
|
+
selectedClients: readonly OcxConnectedClientId[],
|
|
243
|
+
readiness: ClientCatalogReadiness,
|
|
244
|
+
): ConnectCompletionReport {
|
|
245
|
+
const connected = `Connected to ${connection.serverUrl} as key ${connection.apiKeyId}.`;
|
|
246
|
+
if (readiness.kind === "ready") {
|
|
247
|
+
return { lines: [connected, "Local Codex CLI: ready (it accepts every reasoning level in the installed catalog)."], failure: null };
|
|
248
|
+
}
|
|
249
|
+
if (readiness.kind === "unverified") {
|
|
250
|
+
// Not a failure. A client with no observable Codex CLI is a working configuration, and the
|
|
251
|
+
// write-time gate deliberately lets it through; saying so is the honest middle report.
|
|
252
|
+
return { lines: [connected, `Local Codex CLI: unverified (${readiness.reason}).`], failure: null };
|
|
253
|
+
}
|
|
254
|
+
const verdict = `Local Codex CLI: not ready (${readiness.reason})`;
|
|
255
|
+
if (!selectedClients.includes("codex")) {
|
|
256
|
+
return {
|
|
257
|
+
lines: [connected, `${verdict} This connection selected ${selectedClients.join(", ")}, so nothing here launches Codex.`],
|
|
258
|
+
failure: null,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
lines: [verdict, `The connection to ${connection.serverUrl} as key ${connection.apiKeyId} was saved; run 'ocx connect status' to see it.`],
|
|
263
|
+
failure: `client_not_ready: ${readiness.reason}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
116
267
|
function statusLines(status: ClientConnectionStatus): string[] {
|
|
117
268
|
if (status.state !== "connected") {
|
|
118
269
|
return [`Connection: ${status.state}${status.reason ? ` (${status.reason})` : ""}`];
|
|
119
270
|
}
|
|
120
271
|
return [
|
|
121
272
|
"Connection: connected",
|
|
273
|
+
// Second line on purpose. The whole of #4207 is that a reader stopped at "connected" and
|
|
274
|
+
// believed the client was usable, so the local verdict has to arrive before the hub detail.
|
|
275
|
+
readinessLine(status),
|
|
122
276
|
`Hub: ${status.serverUrl}`,
|
|
123
277
|
`Management: ${status.managementUrl} (${status.managementTransport})`,
|
|
124
278
|
`Protocol: ${status.protocolVersion}`,
|
|
@@ -183,8 +337,21 @@ async function runConnect(argv: string[], deps: ClientCommandDeps): Promise<void
|
|
|
183
337
|
managementTransport,
|
|
184
338
|
noSync,
|
|
185
339
|
...(catalogTimeoutSeconds === undefined ? {} : { catalogTimeoutMs: catalogTimeoutSeconds * 1_000 }),
|
|
186
|
-
}, {
|
|
187
|
-
|
|
340
|
+
}, {
|
|
341
|
+
fetchImpl: deps.fetchImpl,
|
|
342
|
+
lifecycleLockDeps: deps.lifecycleLockDeps,
|
|
343
|
+
// Same observer the readiness check below uses. Passed unconditionally: leaving it out in
|
|
344
|
+
// production would let the gate fall back to its own probing, persisting default, so one
|
|
345
|
+
// command could run two probes and act on two different ladders.
|
|
346
|
+
catalogCompatibility: catalogObserver(deps.catalogProbeDeps),
|
|
347
|
+
});
|
|
348
|
+
// The hub and the credential are proven at this point; the local runtime is not. Reporting
|
|
349
|
+
// only the first half is what #4207 was filed for, so the catalog now on disk is checked
|
|
350
|
+
// against the Codex CLI that will read it.
|
|
351
|
+
const readiness = inspectInstalledCatalogReadiness(installedCatalogFileState(), deps.catalogProbeDeps ?? {});
|
|
352
|
+
const report = connectCompletionReport(connection, clients, readiness);
|
|
353
|
+
for (const line of report.lines) console.log(line);
|
|
354
|
+
if (report.failure) throw new Error(report.failure);
|
|
188
355
|
}
|
|
189
356
|
|
|
190
357
|
async function runRevoke(argv: string[], deps: ClientCommandDeps): Promise<void> {
|
|
@@ -204,7 +371,7 @@ export async function handleConnectCommand(argv: string[], deps: ClientCommandDe
|
|
|
204
371
|
const args = argv.slice(1);
|
|
205
372
|
const wantsJson = takeFlag(args, "--json");
|
|
206
373
|
rejectArgs(args, CONNECT_USAGE, { redactValues: true });
|
|
207
|
-
const status = collectClientConnectionStatus(Date.now(), deps.lifecycleLockDeps);
|
|
374
|
+
const status = collectClientConnectionStatus(Date.now(), deps.lifecycleLockDeps, deps.catalogProbeDeps ?? {});
|
|
208
375
|
printData(status, wantsJson, statusLines(status));
|
|
209
376
|
return;
|
|
210
377
|
}
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -16,7 +16,12 @@ import type { LivenessIo, LiveProxy } from "../server/proxy-liveness";
|
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import type { OwnedIntegrationRefreshOutcome } from "../integrations/owned-refresh";
|
|
18
18
|
import { hasHelpFlag, printSubcommandUsage, printUsage } from "./help";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
HUB_GATED_SKIP_MESSAGE,
|
|
21
|
+
localClientSkipMessage,
|
|
22
|
+
setIntegrationEnabled,
|
|
23
|
+
shouldSyncCodexOnStart,
|
|
24
|
+
} from "../codex/desired-state";
|
|
20
25
|
import { syncModelsToCodex } from "../codex/sync";
|
|
21
26
|
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
22
27
|
import { restoreNativeCodexAsync } from "../codex/inject";
|
|
@@ -54,6 +59,24 @@ export interface CliDispatchDeps {
|
|
|
54
59
|
|
|
55
60
|
type CommandRunner = (deps: CliDispatchDeps) => Promise<number>;
|
|
56
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The hub's management ingress is deliberately loopback-only. Prefer it for
|
|
64
|
+
* a browser opened on the hub itself: the proxy listener may be restricted to
|
|
65
|
+
* a Tailscale address, while the ingress is the local authenticated dashboard.
|
|
66
|
+
*/
|
|
67
|
+
export function selectDefaultGuiUrl(
|
|
68
|
+
config: Pick<OcxConfig, "port" | "hostname" | "runtimeRole" | "hub">,
|
|
69
|
+
live: Pick<LiveProxy, "port" | "hostname"> | null,
|
|
70
|
+
probeHostname: (hostname: string | undefined) => string,
|
|
71
|
+
): string {
|
|
72
|
+
const ingress = config.runtimeRole === "hub" ? config.hub?.managementIngress : undefined;
|
|
73
|
+
if (ingress?.enabled) return `http://localhost:${ingress.port}`;
|
|
74
|
+
|
|
75
|
+
const guiHost = probeHostname(live?.hostname ?? config.hostname);
|
|
76
|
+
const hostname = guiHost === "127.0.0.1" ? "localhost" : guiHost;
|
|
77
|
+
return `http://${hostname}:${live?.port ?? config.port ?? 10100}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
const commandRunners: Record<string, CommandRunner> = {
|
|
58
81
|
init: async () => {
|
|
59
82
|
const { runInit } = await import("./init");
|
|
@@ -105,7 +128,17 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
105
128
|
}
|
|
106
129
|
const synced = await syncModelsToCodex(live.port);
|
|
107
130
|
if (synced.status === "skipped") {
|
|
108
|
-
|
|
131
|
+
// `setIntegrationEnabled` above just committed ON, so a skip here is NOT the toggle and
|
|
132
|
+
// is not a competing writer either — on a hub it is the role gate. Telling the operator
|
|
133
|
+
// to "retry after the competing integration change finishes" sent them waiting for a
|
|
134
|
+
// writer that does not exist (#4236).
|
|
135
|
+
return emitBack(
|
|
136
|
+
false,
|
|
137
|
+
synced.skippedReason === "hub-gated"
|
|
138
|
+
? `${HUB_GATED_SKIP_MESSAGE} restore back did not change Codex.`
|
|
139
|
+
: "Codex integration is OFF; restore back did not change Codex. Retry after the competing integration change finishes.",
|
|
140
|
+
2,
|
|
141
|
+
);
|
|
109
142
|
}
|
|
110
143
|
if (!synced.ok) {
|
|
111
144
|
return emitBack(false, "Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.", 1);
|
|
@@ -372,7 +405,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
372
405
|
);
|
|
373
406
|
let code = 0;
|
|
374
407
|
if (synced.status === "skipped") {
|
|
375
|
-
console.log(
|
|
408
|
+
console.log(synced.skippedReason === "hub-gated"
|
|
409
|
+
? `${HUB_GATED_SKIP_MESSAGE} sync skipped and no Codex files changed.`
|
|
410
|
+
: "Codex integration is OFF; sync skipped and no Codex files changed.");
|
|
376
411
|
} else if (synced.status === "catalog-only") {
|
|
377
412
|
// Explicit sync with the integration OFF still refreshes the catalog/cache
|
|
378
413
|
// for side profiles that consume the proxy without injection.
|
|
@@ -448,7 +483,8 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
448
483
|
const { readCodexCatalogPathForHome } = await import("../codex/catalog/parsing");
|
|
449
484
|
const { existsSync } = await import("node:fs");
|
|
450
485
|
const owningCodexHome = getCodexHome();
|
|
451
|
-
const
|
|
486
|
+
const cacheGateSnapshot = deps.loadConfig();
|
|
487
|
+
const desiredDisabled = !shouldSyncCodexOnStart(cacheGateSnapshot);
|
|
452
488
|
const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
453
489
|
invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, { allowWhenDesiredDisabled: true }));
|
|
454
490
|
const cacheJson = cacheArgs.includes("--json");
|
|
@@ -462,7 +498,11 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
462
498
|
} else if (desiredDisabled && !cacheJson) {
|
|
463
499
|
// Worth saying in the human path, because it explains why nothing was written.
|
|
464
500
|
// Under --json this belongs on the envelope, not as a second stdout line.
|
|
465
|
-
console.log(
|
|
501
|
+
console.log(localClientSkipMessage(
|
|
502
|
+
cacheGateSnapshot,
|
|
503
|
+
"Codex integration is OFF; no catalog or cache write resulted.",
|
|
504
|
+
"No catalog or cache write resulted.",
|
|
505
|
+
));
|
|
466
506
|
}
|
|
467
507
|
// `completed` with a falsy value means the cache was NOT rewritten. Previously every
|
|
468
508
|
// outcome exited 0, so a script could not tell a refreshed cache from a skipped one.
|
|
@@ -535,10 +575,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
535
575
|
return 1;
|
|
536
576
|
}
|
|
537
577
|
}
|
|
538
|
-
|
|
539
|
-
// loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
|
|
540
|
-
const guiHost = deps.probeHostname(live?.hostname ?? config.hostname);
|
|
541
|
-
const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
|
|
578
|
+
const guiUrl = selectDefaultGuiUrl(config, live, deps.probeHostname);
|
|
542
579
|
console.log(`Opening ${guiUrl}`);
|
|
543
580
|
const { openUrl } = await import("../lib/open-url");
|
|
544
581
|
openUrl(guiUrl);
|
|
@@ -546,6 +583,13 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
546
583
|
},
|
|
547
584
|
});
|
|
548
585
|
},
|
|
586
|
+
hub: async deps => {
|
|
587
|
+
const { runHubCommand } = await import("./hub");
|
|
588
|
+
return runHubCommand(deps.args.slice(1), {
|
|
589
|
+
loadConfig: deps.loadConfig,
|
|
590
|
+
findLiveProxy: deps.findLiveProxy,
|
|
591
|
+
});
|
|
592
|
+
},
|
|
549
593
|
service: async deps => {
|
|
550
594
|
process.exitCode = 0;
|
|
551
595
|
await deps.serviceCommand(...deps.args.slice(1));
|
package/src/cli/doctor.ts
CHANGED
|
@@ -56,6 +56,8 @@ import {
|
|
|
56
56
|
import { collectStartupHealth, formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
57
57
|
import {
|
|
58
58
|
displayCodexRuntimePath,
|
|
59
|
+
effortClampAppliesToRuntime,
|
|
60
|
+
liveRemovedEfforts,
|
|
59
61
|
loadLastEffortClamp,
|
|
60
62
|
persistCodexRuntime,
|
|
61
63
|
resolveAndPersistCodexRuntime,
|
|
@@ -1177,9 +1179,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1177
1179
|
console.log(" Suggested: set CODEX_CLI_PATH to the desired binary and run ocx sync.");
|
|
1178
1180
|
console.log(" Optional: ocx doctor --fix-codex-runtime");
|
|
1179
1181
|
}
|
|
1182
|
+
// Doctor used to warn on any non-empty `removedEfforts`, while `ocx status` asked
|
|
1183
|
+
// `effortClampAppliesToRuntime` — so the two could disagree about the same file, and doctor
|
|
1184
|
+
// would tell an operator to install a newer Codex while the resolved runtime was already
|
|
1185
|
+
// newer than the one the diagnostic described. Both surfaces now read the same predicate.
|
|
1180
1186
|
const lastClamp = loadLastEffortClamp();
|
|
1181
|
-
if (lastClamp
|
|
1182
|
-
|
|
1187
|
+
if (effortClampAppliesToRuntime(lastClamp, resolved.runtime)) {
|
|
1188
|
+
const live = liveRemovedEfforts(lastClamp);
|
|
1189
|
+
console.log(` !! ${live.join(" and ")} were removed during catalog sync.`);
|
|
1183
1190
|
console.log(" Suggested: set CODEX_CLI_PATH to a newer Codex binary and run ocx sync.");
|
|
1184
1191
|
}
|
|
1185
1192
|
}
|
|
@@ -13,6 +13,8 @@ import { stripGrokConfig, type GrokInjectResult } from "../grok/inject";
|
|
|
13
13
|
import { removeDesktop3pStandardPivot } from "../claude/desktop-3p";
|
|
14
14
|
import {
|
|
15
15
|
claudeDesktopIntegrationEnabled,
|
|
16
|
+
grokIntegrationEnabled,
|
|
17
|
+
HUB_GATED_SKIP_MESSAGE,
|
|
16
18
|
shouldSyncGrokOnStart,
|
|
17
19
|
} from "../codex/desired-state";
|
|
18
20
|
import type { OcxConfig } from "../types";
|
|
@@ -78,6 +80,14 @@ export async function ensureGrokFenceMatchesDesired(
|
|
|
78
80
|
): Promise<void> {
|
|
79
81
|
const config = deps.loadConfig();
|
|
80
82
|
const { log, error } = io(deps);
|
|
83
|
+
// A hub-gated skip is NOT "the user turned Grok off" (#4236). Stripping the managed block
|
|
84
|
+
// there deleted a fence the operator still wants — and `ocx ensure` reported it as the
|
|
85
|
+
// Grok toggle doing its job. Only an explicit OFF authorizes the strip; the gate just
|
|
86
|
+
// declines to write, and says which key would let it.
|
|
87
|
+
if (!shouldSyncGrokOnStart(config) && grokIntegrationEnabled(config)) {
|
|
88
|
+
log(` ${HUB_GATED_SKIP_MESSAGE} ~/.grok/config.toml was left exactly as it is.`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
81
91
|
if (!shouldSyncGrokOnStart(config)) {
|
|
82
92
|
try {
|
|
83
93
|
const grok = deps.stripGrokConfig();
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
GUI_PAIR_NONCE_HEADER,
|
|
18
18
|
GUI_PAIR_PATH,
|
|
19
19
|
canonicalGuiBrowserOrigin,
|
|
20
|
+
canonicalHttpOrigin,
|
|
20
21
|
createGuiPairCapability,
|
|
21
22
|
} from "../lib/gui-pair-capability";
|
|
22
23
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
@@ -52,18 +53,6 @@ function sameRuntime(left: RuntimePortState, right: RuntimePortState | null): bo
|
|
|
52
53
|
&& timingSafeEqual(leftSecret, rightSecret);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function canonicalHttpOrigin(value: unknown): string | null {
|
|
56
|
-
if (typeof value !== "string") return null;
|
|
57
|
-
try {
|
|
58
|
-
const parsed = new URL(value);
|
|
59
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
60
|
-
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) return null;
|
|
61
|
-
return parsed.origin;
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
56
|
function parseCreatedResult(value: unknown, browserOrigin: string): GuiPairRequestResult | null {
|
|
68
57
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
69
58
|
const record = value as Record<string, unknown>;
|
package/src/cli/help.ts
CHANGED
|
@@ -45,7 +45,7 @@ Usage:
|
|
|
45
45
|
ocx sync [--restart-codex] Fetch models from providers and inject into Codex config
|
|
46
46
|
ocx sync-cache [--restart-codex]
|
|
47
47
|
Refresh Codex's model cache from the active catalog
|
|
48
|
-
ocx status Check proxy server status
|
|
48
|
+
ocx status Check proxy server status (on a hub: one block with its ports and token source)
|
|
49
49
|
ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
|
|
50
50
|
ocx doctor --reclaim-response-temps
|
|
51
51
|
Reclaim abandoned response-state temp files (works without a running proxy)
|
|
@@ -56,6 +56,8 @@ Usage:
|
|
|
56
56
|
ocx logout <provider> Remove a stored OAuth login
|
|
57
57
|
ocx gui [pair --origin <browser-origin> [--json]]
|
|
58
58
|
Open the dashboard or create a single-use remote pairing grant
|
|
59
|
+
ocx hub invite [--json] Print a ready-to-run \`ocx connect\` line for one more machine
|
|
60
|
+
(hub only; see \`ocx help hub\` for the one-port topology)
|
|
59
61
|
ocx update [--tag <tag>] Update opencodex (keeps preview installs on @preview)
|
|
60
62
|
ocx restart Stop and restart the proxy
|
|
61
63
|
ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)
|
|
@@ -99,6 +101,7 @@ Examples:
|
|
|
99
101
|
ocx start Start on default port (10100)
|
|
100
102
|
ocx start --port 8080 Start on custom port
|
|
101
103
|
ocx help service Show service command help
|
|
104
|
+
ocx help hub Explain the hub topology, token file, and invites
|
|
102
105
|
ocx sync Sync available models to Codex`);
|
|
103
106
|
}
|
|
104
107
|
|