@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/config.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
MAIN_CODEX_ACCOUNT_NAMESPACE_TARGET,
|
|
47
47
|
} from "./codex/account-namespace-match";
|
|
48
48
|
import { isCodexAccountPriorityKey } from "./codex/account-priority";
|
|
49
|
+
import { loopbackCompanionAllowed } from "./codex/loopback-target";
|
|
49
50
|
import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "./codex/upstream-host-health";
|
|
50
51
|
import {
|
|
51
52
|
adoptCustomModelCatalogMigration,
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
MODEL_ADAPTER_OVERRIDE_ALLOWED,
|
|
74
75
|
OPENAI_PROVIDER_TIER_VERSION,
|
|
75
76
|
pinnedWireAdapter,
|
|
77
|
+
PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS,
|
|
76
78
|
UPSTREAM_HTTP_VERSION_VALUES,
|
|
77
79
|
type OcxClaudeCodeConfig,
|
|
78
80
|
type OcxConfig,
|
|
@@ -497,6 +499,47 @@ export function requestPacingConfigError(value: unknown): string | null {
|
|
|
497
499
|
return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs provider rule or model overrides";
|
|
498
500
|
}
|
|
499
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Bounds for the opt-in passthrough web-search bridge (`providers.<name>.webSearchBridge`,
|
|
504
|
+
* #3761). Strict for the same reason `retryOn429` is: a misspelled key here would silently
|
|
505
|
+
* leave the bridge disarmed while the operator believes they enabled it. `endpoint` is only
|
|
506
|
+
* shape-checked here; `planPassthroughWebSearchBridge` re-validates the origin before any key
|
|
507
|
+
* is sent to it, because config validation is not an authorization boundary.
|
|
508
|
+
*/
|
|
509
|
+
const providerWebSearchBridgeSchema = z.object({
|
|
510
|
+
enabled: z.boolean().optional(),
|
|
511
|
+
backend: z.enum(PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS).optional(),
|
|
512
|
+
maxSearches: z.number().int().min(1).max(10).optional(),
|
|
513
|
+
timeoutMs: z.number().int().min(1_000).max(600_000).optional(),
|
|
514
|
+
endpoint: z.string().min(1).optional(),
|
|
515
|
+
}).strict();
|
|
516
|
+
|
|
517
|
+
export function providerWebSearchBridgeConfigError(value: unknown): string | null {
|
|
518
|
+
if (value === undefined) return null;
|
|
519
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
520
|
+
return "webSearchBridge must be a plain object";
|
|
521
|
+
}
|
|
522
|
+
const parsed = providerWebSearchBridgeSchema.safeParse(value);
|
|
523
|
+
if (!parsed.success) {
|
|
524
|
+
return "webSearchBridge accepts only enabled (boolean), backend "
|
|
525
|
+
+ `(${PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS.join("|")}), maxSearches (1..10), `
|
|
526
|
+
+ "timeoutMs (1000..600000), and endpoint (absolute http(s) URL)";
|
|
527
|
+
}
|
|
528
|
+
const endpoint = parsed.data.endpoint;
|
|
529
|
+
if (endpoint !== undefined) {
|
|
530
|
+
let url: URL;
|
|
531
|
+
try {
|
|
532
|
+
url = new URL(endpoint);
|
|
533
|
+
} catch {
|
|
534
|
+
return "webSearchBridge.endpoint must be an absolute http(s) URL";
|
|
535
|
+
}
|
|
536
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
537
|
+
return "webSearchBridge.endpoint must be an absolute http(s) URL";
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
500
543
|
const fastWireSchema = z.object({
|
|
501
544
|
kind: z.string(),
|
|
502
545
|
canonicalToWire: z.record(z.string().trim(), z.string().trim()),
|
|
@@ -600,6 +643,10 @@ const providerConfigSchema = z.object({
|
|
|
600
643
|
repairInvalidIds: z.boolean().optional(),
|
|
601
644
|
}).strict().optional(),
|
|
602
645
|
responsesSnapshotRepair: z.boolean().optional(),
|
|
646
|
+
// Invalid blocks degrade to "absent" rather than failing the whole config load: an unusable
|
|
647
|
+
// bridge block must never send an operator through invalid-config recovery for an opt-in
|
|
648
|
+
// feature that is off by default. The management write boundary still rejects it loudly.
|
|
649
|
+
webSearchBridge: providerWebSearchBridgeSchema.optional().catch(undefined),
|
|
603
650
|
xaiResponsesXSearch: z.boolean().optional(),
|
|
604
651
|
xaiResponsesDefaultVersion: z.number().int().positive().optional().catch(undefined),
|
|
605
652
|
}).passthrough();
|
|
@@ -990,6 +1037,18 @@ const hubConfigSchema = z.object({
|
|
|
990
1037
|
}
|
|
991
1038
|
return origin;
|
|
992
1039
|
}).optional(),
|
|
1040
|
+
// Same canonical-origin rule as managementPublicOrigin, and deliberately NOT `.catch`ed:
|
|
1041
|
+
// a mistyped data origin must be rejected at write time, because silently dropping it
|
|
1042
|
+
// makes `ocx hub invite` print the `http://<hostname>:<port>` fallback that the operator
|
|
1043
|
+
// set this field precisely to replace.
|
|
1044
|
+
dataPublicOrigin: z.string().transform((value, ctx) => {
|
|
1045
|
+
const origin = canonicalHttpOrigin(value);
|
|
1046
|
+
if (!origin) {
|
|
1047
|
+
ctx.addIssue({ code: "custom", message: "must be a canonical http(s) origin without credentials, path, query, or fragment" });
|
|
1048
|
+
return z.NEVER;
|
|
1049
|
+
}
|
|
1050
|
+
return origin;
|
|
1051
|
+
}).optional(),
|
|
993
1052
|
// A malformed hand edit disables only the optional ingress. Live writes are rejected by
|
|
994
1053
|
// managementIngressConfigError before this load-time degradation can hide the mistake.
|
|
995
1054
|
managementIngress: z.union([
|
|
@@ -1065,6 +1124,17 @@ const clientConnectionSchema = z.object({
|
|
|
1065
1124
|
}).optional(),
|
|
1066
1125
|
}).strict();
|
|
1067
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* Codex pool selection policy section.
|
|
1129
|
+
*
|
|
1130
|
+
* `.strict()` like its neighbour: a typo in an optional feature section should surface as a
|
|
1131
|
+
* rejected write rather than a silently ignored key that leaves the operator believing they
|
|
1132
|
+
* excluded something.
|
|
1133
|
+
*/
|
|
1134
|
+
const codexPoolSchema = z.object({
|
|
1135
|
+
excludedPlans: z.array(z.string().trim().min(1)).optional(),
|
|
1136
|
+
}).strict();
|
|
1137
|
+
|
|
1068
1138
|
/**
|
|
1069
1139
|
* Quota-reset notification section.
|
|
1070
1140
|
*
|
|
@@ -1100,6 +1170,9 @@ const configSchema = z.object({
|
|
|
1100
1170
|
// candidates are rejected explicitly by remoteGuiConfigError below.
|
|
1101
1171
|
hub: hubConfigSchema.optional().catch(undefined),
|
|
1102
1172
|
remoteGui: remoteGuiConfigSchema.optional().catch(undefined),
|
|
1173
|
+
// A malformed privacy block must never be read as "unmask": .catch(undefined) drops it and
|
|
1174
|
+
// emailMaskingEnabled then falls back to masked, which is also what an absent block means.
|
|
1175
|
+
privacy: z.object({ maskEmails: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
1103
1176
|
// A malformed present client block must remain diagnosable from raw config and
|
|
1104
1177
|
// fail closed through src/client/state.ts; unrelated provider state still loads.
|
|
1105
1178
|
client: clientConnectionSchema.optional().catch(undefined),
|
|
@@ -1118,6 +1191,15 @@ const configSchema = z.object({
|
|
|
1118
1191
|
.min(0)
|
|
1119
1192
|
.optional()
|
|
1120
1193
|
.catch(undefined),
|
|
1194
|
+
// Opt-in inbound body ceiling (#3573). An invalid hand edit degrades to the 256 MiB default
|
|
1195
|
+
// rather than failing the parse, matching the outbound guard above: a malformed number must
|
|
1196
|
+
// not change what the proxy admits. The hard ceiling is NOT enforced here — because of that
|
|
1197
|
+
// `.catch`, and because a config object can be built without this schema at all — but in
|
|
1198
|
+
// `resolveInboundBodyLimitBytes()`, which every reader goes through.
|
|
1199
|
+
maxInboundBodyBytes: z.number().int()
|
|
1200
|
+
.min(0)
|
|
1201
|
+
.optional()
|
|
1202
|
+
.catch(undefined),
|
|
1121
1203
|
appOwnedMemoryBudgetMb: z.number().int()
|
|
1122
1204
|
.min(MIN_APP_OWNED_MEMORY_BUDGET_MB)
|
|
1123
1205
|
.max(MAX_APP_OWNED_MEMORY_BUDGET_MB)
|
|
@@ -1130,13 +1212,16 @@ const configSchema = z.object({
|
|
|
1130
1212
|
// is safe: startServer() already falls back to 127.0.0.1 for a missing hostname. Write-time
|
|
1131
1213
|
// rejection lives in validateConfigCandidate() so bad values still surface to the caller.
|
|
1132
1214
|
hostname: z.string().trim().min(1).optional().catch(undefined),
|
|
1133
|
-
// Discriminated on `enabled` so a disabled entry cannot be forced to carry a port
|
|
1134
|
-
// enabled one
|
|
1135
|
-
//
|
|
1136
|
-
//
|
|
1215
|
+
// Discriminated on `enabled` so a disabled entry cannot be forced to carry a port (#1102).
|
|
1216
|
+
// An enabled one MAY omit it: that is the companion form, which binds 127.0.0.1 on the proxy
|
|
1217
|
+
// port and is legal only off a loopback/wildcard bind — a relationship between two fields, so
|
|
1218
|
+
// it is enforced in validateConfigCandidate() and again at startup, not here (#4236).
|
|
1219
|
+
// A malformed value degrades to undefined rather than failing the whole parse: this is an
|
|
1220
|
+
// opt-in convenience surface, and a hand-edit typo here must never reset providers/apiKeys
|
|
1221
|
+
// through the backup-and-defaults repair path.
|
|
1137
1222
|
unauthenticatedLoopbackListener: z.union([
|
|
1138
1223
|
z.object({ enabled: z.literal(false) }),
|
|
1139
|
-
z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535) }),
|
|
1224
|
+
z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535).optional() }),
|
|
1140
1225
|
]).optional().catch(undefined),
|
|
1141
1226
|
providers: z.record(z.string(), providerConfigSchema),
|
|
1142
1227
|
modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
|
|
@@ -1191,6 +1276,10 @@ const configSchema = z.object({
|
|
|
1191
1276
|
codexDesktopAuthless: z.boolean().optional().catch(undefined),
|
|
1192
1277
|
codexClientCompaction: z.boolean().optional().catch(undefined),
|
|
1193
1278
|
pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
|
|
1279
|
+
// A malformed policy degrades to "no policy" rather than failing the parse, so a hand-edited
|
|
1280
|
+
// typo cannot trip the backup-and-defaults repair path and wipe providers or pool accounts.
|
|
1281
|
+
// Silently ignoring it would be its own trap, so the write path rejects it and loadConfig warns.
|
|
1282
|
+
codexPool: codexPoolSchema.optional().catch(undefined),
|
|
1194
1283
|
codexQuotaAutoRefresh: codexQuotaAutoRefreshSchema.optional().catch(undefined),
|
|
1195
1284
|
codexAccountNamespaces: codexAccountNamespacesSchema.optional(),
|
|
1196
1285
|
// Selection order is a preference, not a safety control like pause: a malformed
|
|
@@ -2102,6 +2191,20 @@ function malformedQuotaResetNotifyWarning(rawParsed: unknown): string | null {
|
|
|
2102
2191
|
return `quotaResetNotify${field ? `.${field}` : ""} ignored: invalid quota-reset notification configuration`;
|
|
2103
2192
|
}
|
|
2104
2193
|
|
|
2194
|
+
/**
|
|
2195
|
+
* Same silent-in-the-wrong-direction failure as the notification block: a dropped pool policy means
|
|
2196
|
+
* the accounts the operator meant to exclude keep taking traffic, and the only visible symptom is
|
|
2197
|
+
* traffic going somewhere it was supposed to stop going.
|
|
2198
|
+
*/
|
|
2199
|
+
function malformedCodexPoolWarning(rawParsed: unknown): string | null {
|
|
2200
|
+
const raw = rawConfigRecord(rawParsed);
|
|
2201
|
+
if (!raw || !Object.hasOwn(raw, "codexPool")) return null;
|
|
2202
|
+
const result = codexPoolSchema.safeParse(raw.codexPool);
|
|
2203
|
+
if (result.success) return null;
|
|
2204
|
+
const field = result.error.issues[0]?.path.join(".");
|
|
2205
|
+
return `codexPool${field ? `.${field}` : ""} ignored: invalid Codex pool selection policy`;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2105
2208
|
/**
|
|
2106
2209
|
* Warn once per load that the section was dropped.
|
|
2107
2210
|
*
|
|
@@ -2114,6 +2217,18 @@ function warnDegradedQuotaResetNotify(rawParsed: unknown): void {
|
|
|
2114
2217
|
if (warning) console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
|
|
2115
2218
|
}
|
|
2116
2219
|
|
|
2220
|
+
/**
|
|
2221
|
+
* Warn once per load that the pool policy was dropped.
|
|
2222
|
+
*
|
|
2223
|
+
* `.catch(undefined)` turns a malformed policy into a SUCCESSFUL parse, so without this the proxy
|
|
2224
|
+
* starts, rotates onto the accounts the operator meant to exclude, and prints nothing. The visible
|
|
2225
|
+
* symptom would be traffic going exactly where it was told not to go.
|
|
2226
|
+
*/
|
|
2227
|
+
function warnDegradedCodexPool(rawParsed: unknown): void {
|
|
2228
|
+
const warning = malformedCodexPoolWarning(rawParsed);
|
|
2229
|
+
if (warning) console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2117
2232
|
type NativeSubagentPersistedField = "injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults";
|
|
2118
2233
|
|
|
2119
2234
|
function rawConfigRecord(rawParsed: unknown): Record<string, unknown> | null {
|
|
@@ -2273,6 +2388,7 @@ export function loadConfig(): OcxConfig {
|
|
|
2273
2388
|
warnDegradedRuntimeRole(parsed);
|
|
2274
2389
|
warnDegradedOptionalRemoteBlocks(parsed);
|
|
2275
2390
|
warnDegradedQuotaResetNotify(parsed);
|
|
2391
|
+
warnDegradedCodexPool(parsed);
|
|
2276
2392
|
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
|
|
2277
2393
|
}
|
|
2278
2394
|
// Schema validation failed — merge defaults into the raw object instead of
|
|
@@ -2301,6 +2417,7 @@ export function loadConfig(): OcxConfig {
|
|
|
2301
2417
|
warnDegradedRuntimeRole(parsed);
|
|
2302
2418
|
warnDegradedOptionalRemoteBlocks(parsed);
|
|
2303
2419
|
warnDegradedQuotaResetNotify(parsed);
|
|
2420
|
+
warnDegradedCodexPool(parsed);
|
|
2304
2421
|
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
|
|
2305
2422
|
}
|
|
2306
2423
|
// Still failing, but if every complaint is about one or more named entries
|
|
@@ -2325,6 +2442,7 @@ export function loadConfig(): OcxConfig {
|
|
|
2325
2442
|
warnDegradedRuntimeRole(parsed);
|
|
2326
2443
|
warnDegradedOptionalRemoteBlocks(parsed);
|
|
2327
2444
|
warnDegradedQuotaResetNotify(parsed);
|
|
2445
|
+
warnDegradedCodexPool(parsed);
|
|
2328
2446
|
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
|
|
2329
2447
|
}
|
|
2330
2448
|
}
|
|
@@ -2469,6 +2587,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
|
|
|
2469
2587
|
if (clientWarning) warnings.push(clientWarning);
|
|
2470
2588
|
const notifyWarning = malformedQuotaResetNotifyWarning(rawParsed);
|
|
2471
2589
|
if (notifyWarning) warnings.push(notifyWarning);
|
|
2590
|
+
const codexPoolWarning = malformedCodexPoolWarning(rawParsed);
|
|
2591
|
+
if (codexPoolWarning) warnings.push(codexPoolWarning);
|
|
2472
2592
|
if (syncDisabledReason) {
|
|
2473
2593
|
warnings.push(`syncCodexSubagentDefaults ignored: ${syncDisabledReason}`);
|
|
2474
2594
|
}
|
|
@@ -2617,6 +2737,21 @@ function quotaResetNotifyError(value: unknown): string | null {
|
|
|
2617
2737
|
return `schema_invalid: quotaResetNotify${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
|
|
2618
2738
|
}
|
|
2619
2739
|
|
|
2740
|
+
/**
|
|
2741
|
+
* The read path degrades a malformed pool policy to undefined, which for an exclusion policy means
|
|
2742
|
+
* the excluded accounts quietly keep serving traffic. Reject it on write so `ocx config set` cannot
|
|
2743
|
+
* create a policy that looks applied and is not.
|
|
2744
|
+
*/
|
|
2745
|
+
function codexPoolError(value: unknown): string | null {
|
|
2746
|
+
const raw = rawConfigRecord(value);
|
|
2747
|
+
if (!raw || !Object.hasOwn(raw, "codexPool") || raw.codexPool === undefined) return null;
|
|
2748
|
+
const result = codexPoolSchema.safeParse(raw.codexPool);
|
|
2749
|
+
if (result.success) return null;
|
|
2750
|
+
const issue = result.error.issues[0];
|
|
2751
|
+
const field = issue?.path.join(".");
|
|
2752
|
+
return `schema_invalid: codexPool${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2620
2755
|
/**
|
|
2621
2756
|
* Same reasoning as {@link blankHostnameError}, and more urgent: the read path degrades a
|
|
2622
2757
|
* malformed selection-order map to undefined, which on a write would drop every entry the
|
|
@@ -2701,15 +2836,22 @@ function oauthOpenBrowserError(value: unknown): string | null {
|
|
|
2701
2836
|
|
|
2702
2837
|
/** Validate an in-memory config candidate without touching disk. Used by headless CLI import/set. */
|
|
2703
2838
|
/**
|
|
2704
|
-
* Reject a loopback-listener port that collides with the proxy port (#1102)
|
|
2839
|
+
* Reject a loopback-listener port that collides with the proxy port (#1102), and a port-less
|
|
2840
|
+
* companion listener on a bind address that already owns 127.0.0.1 (#4236).
|
|
2705
2841
|
*
|
|
2706
|
-
* The schema can only check the shape of each field on its own; the two ports being distinct
|
|
2707
|
-
*
|
|
2708
|
-
*
|
|
2842
|
+
* The schema can only check the shape of each field on its own; the two ports being distinct —
|
|
2843
|
+
* and the port-less form being compatible with `hostname` — are relationships between fields.
|
|
2844
|
+
* Letting either through would surface as a startup failure after the public listener already
|
|
2845
|
+
* bound, which reads like an unrelated port conflict.
|
|
2846
|
+
*
|
|
2847
|
+
* Both keys are read from the same candidate, so `ocx config set hostname 127.0.0.1` on a host
|
|
2848
|
+
* whose listener is already the companion form is refused by this same check, with the same
|
|
2849
|
+
* message, rather than breaking the next start.
|
|
2709
2850
|
*
|
|
2710
2851
|
* This is write-time only, matching `blankHostnameError`: a live caller can be told the value
|
|
2711
2852
|
* is wrong, whereas a hand-edited config on the read path degrades to undefined rather than
|
|
2712
|
-
* resetting the whole file.
|
|
2853
|
+
* resetting the whole file. `assertLoopbackListenerBindable` repeats the decision at startup so
|
|
2854
|
+
* a hand edit that skipped this boundary fails with the same sentence instead of EADDRINUSE.
|
|
2713
2855
|
*/
|
|
2714
2856
|
function loopbackListenerPortError(value: unknown): string | null {
|
|
2715
2857
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -2727,17 +2869,46 @@ function loopbackListenerPortError(value: unknown): string | null {
|
|
|
2727
2869
|
return "schema_invalid: unauthenticatedLoopbackListener.enabled: must be a boolean";
|
|
2728
2870
|
}
|
|
2729
2871
|
if (entry.enabled !== true) return null;
|
|
2872
|
+
const hostname = typeof (value as Record<string, unknown>).hostname === "string"
|
|
2873
|
+
? (value as Record<string, unknown>).hostname as string
|
|
2874
|
+
: undefined;
|
|
2875
|
+
const proxyPort = (value as Record<string, unknown>).port;
|
|
2730
2876
|
const listenerPort = entry.port;
|
|
2877
|
+
// The companion form. `port` omitted means "same port as the public listener, on 127.0.0.1",
|
|
2878
|
+
// which only exists as a free address when the public listener is bound somewhere else.
|
|
2879
|
+
if (listenerPort === undefined) {
|
|
2880
|
+
return loopbackCompanionBindError(
|
|
2881
|
+
hostname,
|
|
2882
|
+
typeof proxyPort === "number" ? proxyPort : 10100,
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2731
2885
|
if (typeof listenerPort !== "number" || !Number.isInteger(listenerPort) || listenerPort < 1 || listenerPort > 65535) {
|
|
2732
|
-
return "schema_invalid: unauthenticatedLoopbackListener.port: must be an integer port when enabled";
|
|
2886
|
+
return "schema_invalid: unauthenticatedLoopbackListener.port: must be an integer port when enabled, or omitted to share the proxy port";
|
|
2733
2887
|
}
|
|
2734
|
-
const proxyPort = (value as Record<string, unknown>).port;
|
|
2735
2888
|
if (typeof proxyPort === "number" && proxyPort === listenerPort) {
|
|
2736
2889
|
return "schema_invalid: unauthenticatedLoopbackListener.port: must differ from the proxy port";
|
|
2737
2890
|
}
|
|
2738
2891
|
return null;
|
|
2739
2892
|
}
|
|
2740
2893
|
|
|
2894
|
+
/**
|
|
2895
|
+
* The one sentence both the write boundary and startup use for an impossible companion bind.
|
|
2896
|
+
*
|
|
2897
|
+
* Exported so `startServer` can fail with the identical text: an operator who hand-edited the
|
|
2898
|
+
* file past `validateConfigCandidate` must read the same diagnosis, not EADDRINUSE.
|
|
2899
|
+
*/
|
|
2900
|
+
export function loopbackCompanionBindError(
|
|
2901
|
+
hostname: string | undefined,
|
|
2902
|
+
proxyPort: number,
|
|
2903
|
+
): string | null {
|
|
2904
|
+
if (loopbackCompanionAllowed(hostname)) return null;
|
|
2905
|
+
const bind = (hostname ?? "").trim() || "127.0.0.1";
|
|
2906
|
+
return "schema_invalid: unauthenticatedLoopbackListener: a port-less listener binds "
|
|
2907
|
+
+ `127.0.0.1:${proxyPort}, which the public listener on hostname "${bind}" already holds. `
|
|
2908
|
+
+ "Either set a distinct unauthenticatedLoopbackListener.port, or remove the listener — a "
|
|
2909
|
+
+ "loopback bind already admits local callers without a credential.";
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2741
2912
|
/**
|
|
2742
2913
|
* Validate the hub management ingress at the live-write boundary.
|
|
2743
2914
|
*
|
|
@@ -2792,6 +2963,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
2792
2963
|
?? upstreamHostCircuitThresholdError(value)
|
|
2793
2964
|
?? agentTaskRecoveryError(value)
|
|
2794
2965
|
?? quotaResetNotifyError(value)
|
|
2966
|
+
?? codexPoolError(value)
|
|
2795
2967
|
?? googleAntigravityStaticCatalogVersionError(value)
|
|
2796
2968
|
?? codexAccountPrioritiesError(value)
|
|
2797
2969
|
?? codexQuotaAutoRefreshError(value)
|