@bitkyc08/opencodex 2.58.0 → 2.60.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/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
package/src/cli/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
saveConfig,
|
|
44
44
|
} from "../config";
|
|
45
45
|
import {
|
|
46
|
+
isLikelyOcxProcess,
|
|
46
47
|
readPid,
|
|
47
48
|
readPidFileValue,
|
|
48
49
|
readRuntimePort,
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
import { collectStatus, hubStatusLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
|
|
66
67
|
import { endpointsToProve, everyEndpointProvenDown, sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
|
|
67
68
|
import { takeFlag } from "./runtime-api";
|
|
69
|
+
import { parseStartOptions, StartArgsError } from "./start-args";
|
|
68
70
|
|
|
69
71
|
import {
|
|
70
72
|
discoverStableProxyForRestart,
|
|
@@ -76,9 +78,11 @@ import {
|
|
|
76
78
|
} from "./tray-proxy";
|
|
77
79
|
import { requestBoundSystemRestart } from "./system-restart-client";
|
|
78
80
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
79
|
-
import {
|
|
81
|
+
import { SpendLedgerOwnerError } from "../lib/spend-ledger-owner";
|
|
82
|
+
import { redactUrlForLog } from "../lib/redact";
|
|
83
|
+
import { dispatchCommand, decideBusyPreferredPort, decideStartWithLiveOwner } from "./dispatch";
|
|
80
84
|
import { AuxiliaryListenerBindError, findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
81
|
-
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
85
|
+
import { findLiveProxy, probeHostname, probePortOwner, START_OWNERSHIP_LIVENESS, type LiveProxy } from "../server/proxy-liveness";
|
|
82
86
|
import { createReadinessGate } from "../server/readiness";
|
|
83
87
|
import { isApiAuthRequired } from "../server/auth-cors";
|
|
84
88
|
import { runReady, type ReadyArgs } from "./ready";
|
|
@@ -169,21 +173,13 @@ const head = await runCli(process.argv.slice(2));
|
|
|
169
173
|
const args = head.args;
|
|
170
174
|
const command = head.command;
|
|
171
175
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
const portIdx = args.indexOf("--port");
|
|
179
|
-
if (portIdx === -1) return undefined;
|
|
180
|
-
const value = args[portIdx + 1];
|
|
181
|
-
const port = value && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
182
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
183
|
-
console.error("Invalid port number");
|
|
176
|
+
function parseStartCliOptions(): ReturnType<typeof parseStartOptions> {
|
|
177
|
+
try {
|
|
178
|
+
return parseStartOptions(args.slice(1));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(error instanceof StartArgsError ? error.message : String(error));
|
|
184
181
|
process.exit(1);
|
|
185
182
|
}
|
|
186
|
-
return port;
|
|
187
183
|
}
|
|
188
184
|
|
|
189
185
|
async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
@@ -272,8 +268,41 @@ async function chooseListenPort(
|
|
|
272
268
|
// ever a config collision.
|
|
273
269
|
...(reservedLoopbackPort !== undefined ? { reservedPort: reservedLoopbackPort } : {}),
|
|
274
270
|
});
|
|
275
|
-
if (
|
|
276
|
-
|
|
271
|
+
if (selected !== preferred) {
|
|
272
|
+
// The hop used to be automatic, and that is how a bare `start` beside a healthy
|
|
273
|
+
// proxy produced a second one (#5004): nothing on this path ever asked who held the
|
|
274
|
+
// preferred port. Ask the holder itself — not this home's pid/runtime bookkeeping,
|
|
275
|
+
// which is exactly what was wrong when the duplicate happened — and give the
|
|
276
|
+
// question a budget that cannot mistake one lost probe for an empty port.
|
|
277
|
+
const holder = preferred > 0 && !hardPin
|
|
278
|
+
? await probePortOwner(preferred, { hostname: config.hostname }, START_OWNERSHIP_LIVENESS)
|
|
279
|
+
: null;
|
|
280
|
+
const decision = decideBusyPreferredPort({
|
|
281
|
+
preferredPort: preferred,
|
|
282
|
+
selectedPort: selected,
|
|
283
|
+
hardPin,
|
|
284
|
+
holderIsOpencodex: holder !== null,
|
|
285
|
+
ocxService: process.env.OCX_SERVICE,
|
|
286
|
+
});
|
|
287
|
+
if (decision === "service-stay-out") {
|
|
288
|
+
// Same contract as the pre-bind owner check: the wrapper's retry loop terminates
|
|
289
|
+
// on a zero exit, and the port it was asked to serve is already served.
|
|
290
|
+
console.log(`Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}); service wrapper staying out of the way.`);
|
|
291
|
+
process.exit(0);
|
|
292
|
+
}
|
|
293
|
+
if (decision === "refuse-live-proxy") {
|
|
294
|
+
console.error(`⚠️ Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}). Use 'ocx stop' first.`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
if (decision === "refuse-unidentified-holder") {
|
|
298
|
+
console.error(`❌ Port ${preferred} is busy and its holder did not identify as opencodex.`);
|
|
299
|
+
console.error(" Starting on another port would leave Codex pointed at a proxy you did not ask for.");
|
|
300
|
+
console.error(" Stop whatever holds that port, or start on a free one with 'ocx start --port <port>'.");
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
if (preferred > 0) {
|
|
304
|
+
console.log(`⚠️ Port ${preferred} is busy; starting opencodex on ${selected}.`);
|
|
305
|
+
}
|
|
277
306
|
}
|
|
278
307
|
if (shouldPersistSelectedPort(config.port, selected, preferred, options)) {
|
|
279
308
|
config.port = selected;
|
|
@@ -296,7 +325,12 @@ async function findProxyOwnerBeforeJournalRecovery(
|
|
|
296
325
|
const pidSnapshot = readPidFileValue();
|
|
297
326
|
const hasRuntimeOwner = readRuntimePort() !== null;
|
|
298
327
|
const shouldProbe = pidSnapshot !== null || hasRuntimeOwner || options.probeConfiguredPort === true;
|
|
299
|
-
|
|
328
|
+
// A negative answer here is acted on twice over: the caller walks past a proxy it was
|
|
329
|
+
// supposed to find, and the lines below delete this home's pid record and reconcile the
|
|
330
|
+
// journal. One 750ms probe is not enough evidence for either (#5004) — a transport
|
|
331
|
+
// failure is indistinguishable from an empty port, and the reported Windows duplicate
|
|
332
|
+
// came from exactly that answer on a proxy the previous command had just found healthy.
|
|
333
|
+
const live = shouldProbe ? await findLiveProxy(START_OWNERSHIP_LIVENESS) : null;
|
|
300
334
|
if (live) return { live, pidSnapshot };
|
|
301
335
|
|
|
302
336
|
// The probe established that the snapshotted owner is stale. Compare before
|
|
@@ -328,7 +362,26 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
328
362
|
// already-broken file cannot fence /api/* closed at boot (#2696).
|
|
329
363
|
const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
330
364
|
if (present) assertNotAdminToken(present);
|
|
331
|
-
const
|
|
365
|
+
const startOpts = parseStartCliOptions();
|
|
366
|
+
if (startOpts.socks5 !== undefined || startOpts.socks5Off) {
|
|
367
|
+
const proxyConfig = loadConfig();
|
|
368
|
+
if (startOpts.socks5Off) {
|
|
369
|
+
if (proxyConfig.proxy && !/^socks5h?:\/\//i.test(proxyConfig.proxy.trim())) {
|
|
370
|
+
console.error("Cannot use --socks5-off: config.proxy is not a SOCKS5 URL; it was left unchanged.");
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
if (proxyConfig.proxy) {
|
|
374
|
+
delete proxyConfig.proxy;
|
|
375
|
+
saveConfig(proxyConfig);
|
|
376
|
+
console.log("Cleared config.proxy (outbound SOCKS5 proxy off).");
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
proxyConfig.proxy = startOpts.socks5!;
|
|
380
|
+
saveConfig(proxyConfig);
|
|
381
|
+
console.log(`Outbound SOCKS5: ${redactUrlForLog(startOpts.socks5!)} (saved to config.proxy)`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const requestedPort = startOpts.port;
|
|
332
385
|
// Always probe the configured port, even when both state files are absent. A
|
|
333
386
|
// fallback-port sibling overwrites the pid/runtime records when it starts and
|
|
334
387
|
// removes them on its own shutdown, so their absence proves nothing about the
|
|
@@ -366,8 +419,8 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
366
419
|
// to bake the service (observed: a probe on 10198 left the service pinned there).
|
|
367
420
|
siblingStart = true;
|
|
368
421
|
console.warn(
|
|
369
|
-
`Proxy already running on port ${owner.live.port};
|
|
370
|
-
+ `
|
|
422
|
+
`Proxy already running on port ${owner.live.port}; requested a second instance on port ${requestedPort}. `
|
|
423
|
+
+ `Startup continues only for an independent OPENCODEX_HOME; one state directory has one spend-ledger writer.`,
|
|
371
424
|
);
|
|
372
425
|
}
|
|
373
426
|
|
|
@@ -411,6 +464,10 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
411
464
|
scheduleCatalogPrewarm();
|
|
412
465
|
break;
|
|
413
466
|
} catch (err) {
|
|
467
|
+
if (err instanceof SpendLedgerOwnerError) {
|
|
468
|
+
console.error(`❌ ${err.message}`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
414
471
|
if (err instanceof AuxiliaryListenerBindError || !isAddrInUse(err) || attempt >= 2) throw err;
|
|
415
472
|
if (requestedPort !== undefined) {
|
|
416
473
|
console.log(`⚠️ Port ${port} was taken while starting; waiting to retry the same port...`);
|
|
@@ -927,8 +984,24 @@ async function handleStop() {
|
|
|
927
984
|
// `inheritedTeardowns` is the inverse case: PREVIOUS stops that left obligations
|
|
928
985
|
// unfinished. Snapshot them BEFORE this run claims anything, so this run's own receipt
|
|
929
986
|
// is never mistaken for one it inherited.
|
|
987
|
+
//
|
|
988
|
+
// Ownership is decided by IDENTITY, not by bare liveness. A receipt records a number, and
|
|
989
|
+
// the OS reuses numbers: once the owner exits, an unrelated process can be handed its PID,
|
|
990
|
+
// and `isProcessAlive` alone then answers "that stop is still running" for as long as the
|
|
991
|
+
// new process lives. The receipt is filtered out, so no run ever recovers it, quarantines
|
|
992
|
+
// it or even mentions it — while both updater gates keep seeing an outstanding obligation
|
|
993
|
+
// and refuse. That is the permanent fail-closed reported in #4897: no proxy running, a
|
|
994
|
+
// dead owner, and `ocx update` aborting on `teardown-outstanding` every time.
|
|
995
|
+
//
|
|
996
|
+
// Requiring the live PID to be an opencodex process is the narrowing that costs the safety
|
|
997
|
+
// intent nothing: a stop that really is in flight is still left strictly alone, because its
|
|
998
|
+
// process is one of ours. Recognizing the receipt as abandoned only admits it to the
|
|
999
|
+
// recovery loop below, which still has to prove the recorded endpoint is down before
|
|
1000
|
+
// anything is restored.
|
|
1001
|
+
const teardownOwnerStillRunning = (ownerPid: number): boolean =>
|
|
1002
|
+
isProcessAlive(ownerPid) && isLikelyOcxProcess(ownerPid);
|
|
930
1003
|
const inheritedTeardowns = listPendingTeardowns()
|
|
931
|
-
.filter(read => isPendingTeardownAbandoned(read,
|
|
1004
|
+
.filter(read => isPendingTeardownAbandoned(read, teardownOwnerStillRunning));
|
|
932
1005
|
let teardownNonce: string | undefined;
|
|
933
1006
|
const claimTeardown = (endpoint: { hostname: string; port: number }, endpointSource: "exact" | "guessed") => {
|
|
934
1007
|
if (teardownNonce) return;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
printData,
|
|
5
5
|
rejectArgs,
|
|
6
6
|
runCliAction,
|
|
7
|
+
RuntimeApiError,
|
|
7
8
|
runtimeRequest,
|
|
8
9
|
summaryLines,
|
|
9
10
|
takeBooleanOption,
|
|
@@ -165,6 +166,22 @@ async function priceRequest(write: boolean, argv: string[], deps: RuntimeApiDeps
|
|
|
165
166
|
[auto ? `${selector}: automatic pricing restored.` : `${selector}: manual pricing saved.`]);
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
/**
|
|
170
|
+
* True for the management handler's own unknown-id 404, and only that.
|
|
171
|
+
*
|
|
172
|
+
* Two different listeners answer 404 on this route. `src/server/management/model-routes.ts`
|
|
173
|
+
* means "no custom model with that id"; a listener that does not route the request at all
|
|
174
|
+
* reports `{error, method, path}` (src/client/machine-listener.ts), and runtime-api.ts already
|
|
175
|
+
* renders that shape as a routing statement. Narrowing on the absence of `method`/`path` keeps
|
|
176
|
+
* this rewrite from relabelling a not-served-here 404 as a missing record — the exact confusion
|
|
177
|
+
* #4662 was reported as.
|
|
178
|
+
*/
|
|
179
|
+
function unknownCustomModelId(body: unknown): boolean {
|
|
180
|
+
if (!body || typeof body !== "object") return false;
|
|
181
|
+
const record = body as Record<string, unknown>;
|
|
182
|
+
return record.method === undefined && record.path === undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
168
185
|
async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
169
186
|
const args = [...argv];
|
|
170
187
|
const id = args.shift()?.trim();
|
|
@@ -204,10 +221,22 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
204
221
|
}
|
|
205
222
|
if (defaultEffortRaw !== undefined) patch.defaultReasoningEffort = defaultEffortRaw === "-" ? null : defaultEffortRaw;
|
|
206
223
|
if (Object.keys(patch).length === 0) throw new CliUsageError("at least one edit option is required", USAGE);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
let result: unknown;
|
|
225
|
+
try {
|
|
226
|
+
result = await runtimeRequest(`/api/custom-models/${encodeURIComponent(id)}`, {
|
|
227
|
+
method: "PUT",
|
|
228
|
+
body: JSON.stringify(patch),
|
|
229
|
+
}, deps);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof RuntimeApiError && error.status === 404 && unknownCustomModelId(error.body)) {
|
|
232
|
+
throw new RuntimeApiError(
|
|
233
|
+
`No custom model has id ${id}. Edits address the custom-model id, not the provider/model slug; list the ids with: ocx models list-custom`,
|
|
234
|
+
404,
|
|
235
|
+
error.body,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
211
240
|
printData(result, wantsJson, [`Updated custom model ${id}.`]);
|
|
212
241
|
}
|
|
213
242
|
|
package/src/cli/opencode.ts
CHANGED
|
@@ -41,7 +41,7 @@ import type {
|
|
|
41
41
|
} from "../clients/config-export";
|
|
42
42
|
import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
|
|
43
43
|
import { commandInvocation } from "../lib/win-exec";
|
|
44
|
-
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
44
|
+
import { configuredAdminToken, opencodeCatalogToken } from "../lib/admin-secrets";
|
|
45
45
|
import { localManagementOrigin } from "../lib/local-destinations";
|
|
46
46
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
47
47
|
import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
@@ -684,7 +684,7 @@ export async function cmdOpencode(args: string[]): Promise<number> {
|
|
|
684
684
|
try {
|
|
685
685
|
const managementToken = configuredAdminToken();
|
|
686
686
|
if (!managementToken) throw new Error("No local admin token is available; check the running proxy's home.");
|
|
687
|
-
proxyModels = await fetchOpencodeProxyModels(live, managementToken, {
|
|
687
|
+
proxyModels = await fetchOpencodeProxyModels(live, opencodeCatalogToken(managementToken), {
|
|
688
688
|
managementOrigin: localManagementOrigin({ ...startupConfig, hostname: live.hostname }, live.port),
|
|
689
689
|
});
|
|
690
690
|
} catch (error) {
|
package/src/cli/provider.ts
CHANGED
|
@@ -139,7 +139,7 @@ function handleList(args: string[]): void {
|
|
|
139
139
|
// provider add
|
|
140
140
|
// ---------------------------------------------------------------------------
|
|
141
141
|
|
|
142
|
-
const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--api-key-transport <x-api-key|bearer>] [--default-model <model>] [--model <id> --text-only] [--allow-private-network] [--set-default] [--force] [--json] [--sync]";
|
|
142
|
+
const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--api-key-transport <x-api-key|bearer>] [--default-model <model>] [--model <id> --text-only] [--google-tool-schema-policy <compatible|reject-lossy>] [--allow-private-network] [--set-default] [--force] [--json] [--sync]";
|
|
143
143
|
|
|
144
144
|
async function handleAdd(args: string[]): Promise<void> {
|
|
145
145
|
const name = args[0];
|
|
@@ -164,6 +164,7 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
164
164
|
const adapter = consumeFlagValue(restArgs, "--adapter");
|
|
165
165
|
const baseUrl = consumeFlagValue(restArgs, "--base-url");
|
|
166
166
|
const defaultModel = consumeFlagValue(restArgs, "--default-model");
|
|
167
|
+
const googleToolSchemaPolicy = consumeFlagValue(restArgs, "--google-tool-schema-policy");
|
|
167
168
|
const textOnly = consumeFlag(restArgs, "--text-only");
|
|
168
169
|
const capabilityModel = consumeFlagValue(restArgs, "--model");
|
|
169
170
|
rejectUnknownArgs(restArgs, ADD_USAGE);
|
|
@@ -228,6 +229,17 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
228
229
|
}
|
|
229
230
|
provConfig.apiKeyTransport = apiKeyTransport;
|
|
230
231
|
}
|
|
232
|
+
if (googleToolSchemaPolicy !== undefined) {
|
|
233
|
+
if (googleToolSchemaPolicy !== "compatible" && googleToolSchemaPolicy !== "reject-lossy") {
|
|
234
|
+
console.error('Error: --google-tool-schema-policy must be "compatible" or "reject-lossy".');
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
if (provConfig.adapter !== "google") {
|
|
238
|
+
console.error("Error: --google-tool-schema-policy requires the google adapter.");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
provConfig.googleToolSchemaPolicy = googleToolSchemaPolicy;
|
|
242
|
+
}
|
|
231
243
|
|
|
232
244
|
const existingProvider = config.providers[name];
|
|
233
245
|
if (existingProvider?.modelCapabilities !== undefined && provConfig.modelCapabilities === undefined) {
|
package/src/cli/registry.ts
CHANGED
|
@@ -20,7 +20,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
20
20
|
usage: "ocx setup",
|
|
21
21
|
summary: "Interactive setup for providers and Codex config injection (alias of init).",
|
|
22
22
|
},
|
|
23
|
-
{
|
|
23
|
+
{
|
|
24
|
+
name: "start",
|
|
25
|
+
usage: "ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]",
|
|
26
|
+
summary: "Start the proxy server and sync models to Codex.",
|
|
27
|
+
details: [
|
|
28
|
+
"--socks5 [host:port] Route outbound provider traffic through SOCKS5 (default 127.0.0.1:10808). Saved as config.proxy.",
|
|
29
|
+
"--socks5-off Clear a saved SOCKS5 outbound proxy from config.proxy.",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
24
32
|
{ name: "stop", usage: "ocx stop", summary: "Stop the proxy and restore native Codex config." },
|
|
25
33
|
{
|
|
26
34
|
name: "restore",
|
|
@@ -390,6 +398,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
390
398
|
details: [
|
|
391
399
|
"system update manages OpenCodex itself.",
|
|
392
400
|
"ocx system codex-cli-update check [--json]",
|
|
401
|
+
"ocx system codex-cli-update attest [--json]",
|
|
402
|
+
"ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]",
|
|
393
403
|
"The Codex CLI inspection command makes no package-registry request, does not execute Codex or npm, install or repair software, control a process, or write configuration or cache state.",
|
|
394
404
|
],
|
|
395
405
|
},
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -42,10 +42,39 @@ export class RuntimeApiError extends Error {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Refusal for a management request that resolved to a connected-client machine listener.
|
|
47
|
+
*
|
|
48
|
+
* A connected client runs `src/client/machine-listener.ts`, which binds the SAME address the
|
|
49
|
+
* standalone proxy would (`port ?? config.port ?? 10100`) and answers `/healthz` as opencodex.
|
|
50
|
+
* Liveness therefore finds it — correctly, it is our process — but it serves only
|
|
51
|
+
* `/api/machine/*` and returns a JSON 404 for every other `/api/*` path. Before this refusal
|
|
52
|
+
* every management-backed subcommand on such a machine died on that opaque 404:
|
|
53
|
+
* `{"error":"not_found","method":"PUT","path":"/api/custom-models/<id>"}`, one character away
|
|
54
|
+
* from the real handler's unknown-id `{"error":"not found"}` and indistinguishable from it
|
|
55
|
+
* (#4662). The role was already on the wire; only the parser was throwing it away.
|
|
56
|
+
*
|
|
57
|
+
* 503 rather than 404: the management plane is unavailable here, the resource is not missing,
|
|
58
|
+
* and `runCliAction` maps 404 to exit 4 ("no such thing") — the wrong answer to give a script.
|
|
59
|
+
*/
|
|
60
|
+
function clientRoleManagementRefusal(port: number): string {
|
|
61
|
+
return [
|
|
62
|
+
`The opencodex listener on port ${port} is running in the client role. It serves only the machine routes (/api/machine/*), so this machine has no management API to call.`,
|
|
63
|
+
"Custom-model edits and other management changes are made on the hub this machine is connected to: run the command there, or use the hub dashboard (the dashboard on this machine relays to it when the connection uses the relay transport).",
|
|
64
|
+
"To change this machine's own configuration instead: edit customModels in config.json, then run: ocx sync",
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string> {
|
|
46
69
|
if (deps.baseUrl) return deps.baseUrl.replace(/\/$/, "");
|
|
47
70
|
const live = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
48
71
|
if (!live) throw new RuntimeApiError("Proxy is not running. Start it with: ocx start", 503, null);
|
|
72
|
+
// The role comes from the same identity-checked /healthz body liveness already parsed, so
|
|
73
|
+
// this costs no extra request. Only the client role is refused: an absent role is a
|
|
74
|
+
// standalone or hub proxy (or a legacy body that predates the field), and both serve /api/*.
|
|
75
|
+
if (live.role === "client") {
|
|
76
|
+
throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null);
|
|
77
|
+
}
|
|
49
78
|
return `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
50
79
|
}
|
|
51
80
|
|
|
@@ -62,11 +91,26 @@ function stringField(record: Record<string, unknown>, key: string): string | und
|
|
|
62
91
|
* is unavailable). Both were dropped here, so a fenced management plane was
|
|
63
92
|
* indistinguishable from a generic failure and an operator had no way to tell a port
|
|
64
93
|
* collision from an ACL refusal from a stopped proxy (#2698).
|
|
94
|
+
*
|
|
95
|
+
* A 404 body that carries both `method` and `path` is a different statement again: some
|
|
96
|
+
* opencodex listener answered, and it does not route that request at all. Only the
|
|
97
|
+
* connected-client machine listener emits that shape today (`json404` in
|
|
98
|
+
* src/client/machine-listener.ts), and printing its bare `not_found` token read as though the
|
|
99
|
+
* resource were missing — a real handler's unknown-id 404 says `not found`, one space apart
|
|
100
|
+
* (#4662). Name the route instead, so any listener that does not serve a path stays legible
|
|
101
|
+
* even if another one starts answering this way.
|
|
65
102
|
*/
|
|
66
103
|
function responseMessage(body: unknown, status: number): string {
|
|
67
104
|
if (typeof body === "string" && body.trim()) return body.trim().slice(0, 400);
|
|
68
105
|
if (!body || typeof body !== "object") return `Management request failed (${status})`;
|
|
69
106
|
const record = body as Record<string, unknown>;
|
|
107
|
+
if (status === 404) {
|
|
108
|
+
const method = stringField(record, "method");
|
|
109
|
+
const path = stringField(record, "path");
|
|
110
|
+
if (method && path) {
|
|
111
|
+
return `This opencodex listener does not serve ${method.slice(0, 16)} ${path.slice(0, 200)}, so the request was refused before any handler ran (it is not a missing record). Check that the command is pointed at a proxy that serves the management API.`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
70
114
|
let primary: string | undefined;
|
|
71
115
|
for (const key of ["error", "message", "detail"]) {
|
|
72
116
|
primary = stringField(record, key);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export const START_USAGE = "Usage: ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]";
|
|
2
|
+
export const START_USAGE_LINE = "ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]";
|
|
3
|
+
export const DEFAULT_SOCKS5_PROXY = "socks5://127.0.0.1:10808";
|
|
4
|
+
|
|
5
|
+
export class StartArgsError extends Error {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "StartArgsError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type StartOptions = {
|
|
13
|
+
port?: number;
|
|
14
|
+
/** SOCKS5 URL to persist. */
|
|
15
|
+
socks5?: string;
|
|
16
|
+
socks5Off?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function isSocksProxyUrl(proxy: string): boolean {
|
|
20
|
+
return /^socks5h?:\/\//i.test(proxy.trim());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeSocks5(raw: string): string {
|
|
24
|
+
const trimmed = raw.trim();
|
|
25
|
+
if (!trimmed) return DEFAULT_SOCKS5_PROXY;
|
|
26
|
+
if (isSocksProxyUrl(trimmed)) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(trimmed);
|
|
29
|
+
const port = Number(parsed.port);
|
|
30
|
+
if (!parsed.hostname || !parsed.port || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
31
|
+
throw new Error("invalid host or port");
|
|
32
|
+
}
|
|
33
|
+
return trimmed;
|
|
34
|
+
} catch {
|
|
35
|
+
throw new StartArgsError("Invalid SOCKS5 address; expected socks5://host:port");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (/^socks4a?:\/\//i.test(trimmed)) {
|
|
39
|
+
throw new StartArgsError("Only SOCKS5 proxy URLs are supported");
|
|
40
|
+
}
|
|
41
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
42
|
+
throw new StartArgsError("SOCKS5 proxy must be socks5://host:port, not an HTTP URL");
|
|
43
|
+
}
|
|
44
|
+
if (/^\d+$/.test(trimmed)) {
|
|
45
|
+
const port = Number(trimmed);
|
|
46
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
47
|
+
throw new StartArgsError("Invalid SOCKS5 port number");
|
|
48
|
+
}
|
|
49
|
+
return `socks5://127.0.0.1:${port}`;
|
|
50
|
+
}
|
|
51
|
+
const hostPort = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(trimmed);
|
|
52
|
+
if (hostPort) {
|
|
53
|
+
const port = Number(hostPort[2]);
|
|
54
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
55
|
+
throw new StartArgsError("Invalid SOCKS5 port number");
|
|
56
|
+
}
|
|
57
|
+
return `socks5://${trimmed}`;
|
|
58
|
+
}
|
|
59
|
+
throw new StartArgsError("Invalid SOCKS5 address; expected socks5://host:port");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseStartOptions(argv: string[]): StartOptions {
|
|
63
|
+
const options: StartOptions = {};
|
|
64
|
+
for (let i = 0; i < argv.length; i++) {
|
|
65
|
+
const arg = argv[i]!;
|
|
66
|
+
if (arg === "--port") {
|
|
67
|
+
const value = argv[++i];
|
|
68
|
+
const port = value && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
69
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
70
|
+
throw new StartArgsError("Invalid port number");
|
|
71
|
+
}
|
|
72
|
+
options.port = port;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--socks5") {
|
|
76
|
+
if (options.socks5Off) throw new StartArgsError("--socks5 and --socks5-off cannot be used together");
|
|
77
|
+
const next = argv[i + 1];
|
|
78
|
+
if (next && !next.startsWith("-")) {
|
|
79
|
+
options.socks5 = normalizeSocks5(next);
|
|
80
|
+
i += 1;
|
|
81
|
+
} else {
|
|
82
|
+
options.socks5 = DEFAULT_SOCKS5_PROXY;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--socks5-off") {
|
|
87
|
+
if (options.socks5 !== undefined) throw new StartArgsError("--socks5 and --socks5-off cannot be used together");
|
|
88
|
+
options.socks5Off = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
throw new StartArgsError(START_USAGE);
|
|
92
|
+
}
|
|
93
|
+
return options;
|
|
94
|
+
}
|
|
@@ -21,6 +21,8 @@ const USAGE = `Usage:
|
|
|
21
21
|
ocx system codex-app-server [--json]
|
|
22
22
|
ocx system codex-restart --yes [--json]
|
|
23
23
|
ocx system codex-cli-update check [--json]
|
|
24
|
+
ocx system codex-cli-update attest [--json]
|
|
25
|
+
ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]
|
|
24
26
|
ocx system update check [--channel <latest|preview>] [--json]
|
|
25
27
|
ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
|
|
26
28
|
ocx system update status <job-id> [--json]
|
|
@@ -23,7 +23,7 @@ export interface MachineStatusV1 {
|
|
|
23
23
|
export interface MachineApiDeps {
|
|
24
24
|
sync: typeof syncConnectedClient;
|
|
25
25
|
disconnect: typeof disconnectClient;
|
|
26
|
-
scheduleStandaloneRecycle: () => void;
|
|
26
|
+
scheduleStandaloneRecycle: (disconnectedTokenFingerprint: string) => void;
|
|
27
27
|
hubReachability?: () => HubReachability;
|
|
28
28
|
setHubReachability?: (value: HubReachability) => void;
|
|
29
29
|
}
|
|
@@ -74,10 +74,11 @@ export async function handleMachineApi(
|
|
|
74
74
|
injected: MachineApiDeps = defaultDeps,
|
|
75
75
|
): Promise<Response | null> {
|
|
76
76
|
const deps = { ...defaultDeps, ...injected };
|
|
77
|
-
|
|
77
|
+
const readMethod = req.method === "GET" || req.method === "HEAD";
|
|
78
|
+
if (url.pathname === "/api/machine/status" && readMethod) {
|
|
78
79
|
return Response.json(statusPayload(req, state, deps), { headers: { "Cache-Control": "no-store" } });
|
|
79
80
|
}
|
|
80
|
-
if (url.pathname === "/api/machine/clients" &&
|
|
81
|
+
if (url.pathname === "/api/machine/clients" && readMethod) {
|
|
81
82
|
return Response.json({
|
|
82
83
|
selectedClients: [...state.selectedClients],
|
|
83
84
|
journalOwner: journalOwner(),
|
|
@@ -103,7 +104,7 @@ export async function handleMachineApi(
|
|
|
103
104
|
return Response.json({ success: false, error: message }, { status: 502 });
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
if (url.pathname === "/api/machine/shim" &&
|
|
107
|
+
if (url.pathname === "/api/machine/shim" && readMethod) {
|
|
107
108
|
return Response.json(diagnoseCodexShim(), { headers: { "Cache-Control": "no-store" } });
|
|
108
109
|
}
|
|
109
110
|
if (url.pathname === "/api/machine/shim" && req.method === "POST") {
|
|
@@ -129,7 +130,7 @@ export async function handleMachineApi(
|
|
|
129
130
|
}
|
|
130
131
|
try {
|
|
131
132
|
const result = await deps.disconnect(input.keepCatalog === undefined ? {} : { keepCatalog: input.keepCatalog });
|
|
132
|
-
deps.scheduleStandaloneRecycle();
|
|
133
|
+
deps.scheduleStandaloneRecycle(state.tokenFingerprint);
|
|
133
134
|
return Response.json({ success: true, ...result }, { status: 202 });
|
|
134
135
|
} catch (error) {
|
|
135
136
|
return Response.json({ success: false, error: error instanceof Error ? error.message : "disconnect failed" }, { status: 409 });
|
|
@@ -46,9 +46,14 @@ export function machineRouteAllowed(url: URL, req: Request, relayEnabled: boolea
|
|
|
46
46
|
if (req.headers.get("upgrade")) return false;
|
|
47
47
|
const path = url.pathname;
|
|
48
48
|
if (req.method === "GET" && (path === "/healthz" || path === "/readyz" || path === "/" || path === "/opencodex-session")) return true;
|
|
49
|
-
if (req.method === "GET" && (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim")) return true;
|
|
49
|
+
if ((req.method === "GET" || req.method === "HEAD") && (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim")) return true;
|
|
50
50
|
if (req.method === "POST" && (path === "/api/machine/sync" || path === "/api/machine/shim" || path === "/api/machine/disconnect")) return true;
|
|
51
51
|
if (relayEnabled && path.startsWith("/api/machine/hub-relay/")) return true;
|
|
52
|
+
// Known machine endpoints are admitted for every method so an unsupported
|
|
53
|
+
// method reaches the authenticated method restriction (403) instead of a
|
|
54
|
+
// bare 404 that hides the endpoint entirely.
|
|
55
|
+
if (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim"
|
|
56
|
+
|| path === "/api/machine/sync" || path === "/api/machine/disconnect") return true;
|
|
52
57
|
if (req.method !== "GET" || path.startsWith("/api/") || path.startsWith("/v1/")) return false;
|
|
53
58
|
return GUI_SPA_PATHS.has(path)
|
|
54
59
|
|| path.startsWith("/integrations/")
|
|
@@ -70,8 +75,8 @@ export function startMachineListener(
|
|
|
70
75
|
const machineApiDeps: MachineApiDeps = {
|
|
71
76
|
sync: deps.machineApi?.sync ?? syncConnectedClient,
|
|
72
77
|
disconnect: deps.machineApi?.disconnect ?? disconnectClient,
|
|
73
|
-
scheduleStandaloneRecycle: deps.machineApi?.scheduleStandaloneRecycle ?? (
|
|
74
|
-
void import("./runtime").then(module => module.scheduleStandaloneRecycle());
|
|
78
|
+
scheduleStandaloneRecycle: deps.machineApi?.scheduleStandaloneRecycle ?? (tokenFingerprint => {
|
|
79
|
+
void import("./runtime").then(module => module.scheduleStandaloneRecycle(tokenFingerprint));
|
|
75
80
|
}),
|
|
76
81
|
hubReachability: deps.machineApi?.hubReachability ?? (() => hubReachability),
|
|
77
82
|
setHubReachability: deps.machineApi?.setHubReachability ?? (value => { hubReachability = value; }),
|
|
@@ -111,6 +116,14 @@ export function startMachineListener(
|
|
|
111
116
|
if (managementPrincipal(req, managementAuth, config) !== "gui-session") {
|
|
112
117
|
return Response.json({ error: "opencodex machine GUI session required" }, { status: 401 });
|
|
113
118
|
}
|
|
119
|
+
// A loopback dashboard session proves possession, not user presence: any local
|
|
120
|
+
// process can fetch the dashboard bootstrap and replay its token and CSRF value.
|
|
121
|
+
// Keep the connected listener useful for status/diagnostics, but never let that
|
|
122
|
+
// credentialless bootstrap authorize durable machine changes. Those operations
|
|
123
|
+
// remain available through the explicit CLI commands.
|
|
124
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
125
|
+
return Response.json({ error: "opencodex machine changes require the local CLI" }, { status: 403 });
|
|
126
|
+
}
|
|
114
127
|
return await handleMachineApi(req, url, connection, machineApiDeps) ?? json404(req);
|
|
115
128
|
}
|
|
116
129
|
|
package/src/client/runtime.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { loadConfig } from "../config";
|
|
|
4
4
|
import { removePid, removeRuntimePort, writePid, writeRuntimePort } from "../config/process-state";
|
|
5
5
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
6
6
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
7
|
+
import { loadServiceTokenFromFile, serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
7
8
|
import { findAvailablePort } from "../server/ports";
|
|
8
9
|
import { startMachineListener } from "./machine-listener";
|
|
9
10
|
import { readClientConnectionState } from "./state";
|
|
@@ -17,7 +18,30 @@ function cleanup(): void {
|
|
|
17
18
|
removeRuntimePort(process.pid);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export function
|
|
21
|
+
export function standaloneRecycleEnv(
|
|
22
|
+
env: NodeJS.ProcessEnv,
|
|
23
|
+
disconnectedTokenFingerprint: string,
|
|
24
|
+
): NodeJS.ProcessEnv {
|
|
25
|
+
const childEnv = { ...env };
|
|
26
|
+
const admissionToken = childEnv.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
27
|
+
if (admissionToken && serviceApiTokenFingerprint(admissionToken) !== disconnectedTokenFingerprint) {
|
|
28
|
+
// A surviving env token shadows OCX_API_TOKEN_FILE entirely, so nothing below can
|
|
29
|
+
// reintroduce the disconnected token.
|
|
30
|
+
return childEnv;
|
|
31
|
+
}
|
|
32
|
+
if (admissionToken) delete childEnv.OPENCODEX_API_AUTH_TOKEN;
|
|
33
|
+
// The respawned `ocx start` loads OCX_API_TOKEN_FILE into OPENCODEX_API_AUTH_TOKEN when the
|
|
34
|
+
// env token is absent, so vet the file by content: drop the pointer when it is unreadable or
|
|
35
|
+
// holds the disconnected token, keep it when it holds a different operator credential.
|
|
36
|
+
if (!childEnv.OCX_API_TOKEN_FILE?.trim()) return childEnv;
|
|
37
|
+
const fileToken = loadServiceTokenFromFile(childEnv);
|
|
38
|
+
if (fileToken === null || serviceApiTokenFingerprint(fileToken) === disconnectedTokenFingerprint) {
|
|
39
|
+
delete childEnv.OCX_API_TOKEN_FILE;
|
|
40
|
+
}
|
|
41
|
+
return childEnv;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function scheduleStandaloneRecycle(disconnectedTokenFingerprint: string): void {
|
|
21
45
|
if (recycleScheduled) return;
|
|
22
46
|
recycleScheduled = true;
|
|
23
47
|
const timer = setTimeout(() => {
|
|
@@ -46,7 +70,7 @@ export function scheduleStandaloneRecycle(): void {
|
|
|
46
70
|
detached: true,
|
|
47
71
|
stdio: "ignore",
|
|
48
72
|
windowsHide: true,
|
|
49
|
-
env:
|
|
73
|
+
env: standaloneRecycleEnv(process.env, disconnectedTokenFingerprint),
|
|
50
74
|
});
|
|
51
75
|
child.unref();
|
|
52
76
|
}
|
|
@@ -37,9 +37,8 @@ export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`;
|
|
|
37
37
|
export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* unset, which would silently ship a bogus credential — so we never emit it.
|
|
40
|
+
* Legacy name retained for existing consumers. Gajae exports now use the
|
|
41
|
+
* non-secret loopback placeholder and do not emit an environment reference.
|
|
43
42
|
*/
|
|
44
43
|
export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY";
|
|
45
44
|
|