@bitkyc08/opencodex 2.65.0 → 2.66.0-preview.20260925
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 +14 -0
- package/gui/dist/assets/App-BVjA0T6O.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/index-vO4dgfiU.js +86 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
package/src/client/runtime.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import type { Server } from "bun";
|
|
3
4
|
import { loadConfig } from "../config";
|
|
4
5
|
import { removePid, removeRuntimePort, writePid, writeRuntimePort } from "../config/process-state";
|
|
5
6
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
6
7
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
7
8
|
import { loadServiceTokenFromFile, serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
8
|
-
import { findAvailablePort } from "../server/ports";
|
|
9
|
+
import { findAvailablePort, PortUnavailableError } from "../server/ports";
|
|
10
|
+
import { createClientLinkSupervisor, type ClientLinkSupervisor } from "./link-tunnel";
|
|
11
|
+
import { clientLinkStatePath } from "./link-state";
|
|
9
12
|
import { startMachineListener } from "./machine-listener";
|
|
10
|
-
import { readClientConnectionState } from "./state";
|
|
13
|
+
import { isLinkConnection, readClientConnectionState } from "./state";
|
|
11
14
|
|
|
12
15
|
let activeServer: Server<unknown> | null = null;
|
|
13
16
|
let activePort: number | null = null;
|
|
17
|
+
let activeSupervisor: ClientLinkSupervisor | null = null;
|
|
14
18
|
let recycleScheduled = false;
|
|
15
19
|
|
|
16
20
|
function cleanup(): void {
|
|
@@ -45,56 +49,89 @@ export function scheduleStandaloneRecycle(disconnectedTokenFingerprint: string):
|
|
|
45
49
|
if (recycleScheduled) return;
|
|
46
50
|
recycleScheduled = true;
|
|
47
51
|
const timer = setTimeout(() => {
|
|
48
|
-
|
|
49
|
-
try { activeServer?.stop(true); } catch { /* best effort */ }
|
|
50
|
-
cleanup();
|
|
51
|
-
// Recycling back to standalone after `ocx disconnect` must actually bring a standalone
|
|
52
|
-
// proxy back, under either launch shape.
|
|
53
|
-
//
|
|
54
|
-
// Unsupervised: spawn the replacement ourselves and exit 0.
|
|
55
|
-
//
|
|
56
|
-
// Supervised (`OCX_SERVICE=1`): do NOT spawn — the supervisor owns the process, and a
|
|
57
|
-
// second copy would fight it for the port. But exit 0 does not work either: the real
|
|
58
|
-
// supervisor configs are failure-only (systemd `Restart=on-failure`, WinSW
|
|
59
|
-
// `<onfailure action="restart"/>`, the Task Scheduler ERRORLEVEL loop), so a clean exit
|
|
60
|
-
// reads as "the service finished" and nothing restarts. The client stayed down until the
|
|
61
|
-
// operator noticed. Exit 1 is what those configs are watching for, and it is the same
|
|
62
|
-
// policy the dashboard recycle already uses (src/server/management/system-restart.ts).
|
|
63
|
-
//
|
|
64
|
-
// launchd's KeepAlive restarts on any exit, so it is correct under both branches.
|
|
65
|
-
if (process.env.OCX_SERVICE === "1") {
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
if (port) {
|
|
69
|
-
const child = spawn(process.execPath, selfLaunchArgv(["start", "--port", String(port)]), {
|
|
70
|
-
detached: true,
|
|
71
|
-
stdio: "ignore",
|
|
72
|
-
windowsHide: true,
|
|
73
|
-
env: standaloneRecycleEnv(process.env, disconnectedTokenFingerprint),
|
|
74
|
-
});
|
|
75
|
-
child.unref();
|
|
76
|
-
}
|
|
77
|
-
process.exit(0);
|
|
52
|
+
void recycleStandalone(disconnectedTokenFingerprint);
|
|
78
53
|
}, 50);
|
|
79
54
|
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
80
55
|
}
|
|
81
56
|
|
|
57
|
+
async function recycleStandalone(disconnectedTokenFingerprint: string): Promise<void> {
|
|
58
|
+
const port = activePort;
|
|
59
|
+
try {
|
|
60
|
+
await activeSupervisor?.stop();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn(`[client] link supervisor stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
|
+
}
|
|
64
|
+
activeSupervisor = null;
|
|
65
|
+
try {
|
|
66
|
+
activeServer?.stop(true);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn(`[client] listener stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
+
}
|
|
70
|
+
cleanup();
|
|
71
|
+
// Recycling back to standalone after `ocx disconnect` must actually bring a standalone
|
|
72
|
+
// proxy back, under either launch shape.
|
|
73
|
+
//
|
|
74
|
+
// Unsupervised: spawn the replacement ourselves and exit 0.
|
|
75
|
+
//
|
|
76
|
+
// Supervised (`OCX_SERVICE=1`): do NOT spawn — the supervisor owns the process, and a
|
|
77
|
+
// second copy would fight it for the port. But exit 0 does not work either: the real
|
|
78
|
+
// supervisor configs are failure-only (systemd `Restart=on-failure`, WinSW
|
|
79
|
+
// `<onfailure action="restart"/>`, the Task Scheduler ERRORLEVEL loop), so a clean exit
|
|
80
|
+
// reads as "the service finished" and nothing restarts. The client stayed down until the
|
|
81
|
+
// operator noticed. Exit 1 is what those configs are watching for, and it is the same
|
|
82
|
+
// policy the dashboard recycle already uses (src/server/management/system-restart.ts).
|
|
83
|
+
//
|
|
84
|
+
// launchd's KeepAlive restarts on any exit, so it is correct under both branches.
|
|
85
|
+
if (process.env.OCX_SERVICE === "1") {
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
if (port) {
|
|
89
|
+
const child = spawn(process.execPath, selfLaunchArgv(["start", "--port", String(port)]), {
|
|
90
|
+
detached: true,
|
|
91
|
+
stdio: "ignore",
|
|
92
|
+
windowsHide: true,
|
|
93
|
+
env: standaloneRecycleEnv(process.env, disconnectedTokenFingerprint),
|
|
94
|
+
});
|
|
95
|
+
child.unref();
|
|
96
|
+
}
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
export async function startClientRuntime(
|
|
83
101
|
options: { port?: number; block?: boolean } = {},
|
|
84
102
|
): Promise<void> {
|
|
85
103
|
const state = readClientConnectionState();
|
|
86
104
|
if (state.kind !== "connected") throw new Error(`client runtime refused: client state is ${state.kind}`);
|
|
87
105
|
const config = loadConfig();
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
const linkMode = isLinkConnection(state.value);
|
|
107
|
+
if (linkMode && (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535)) {
|
|
108
|
+
throw new Error("link mode requires a valid local config port");
|
|
109
|
+
}
|
|
110
|
+
const preferred = linkMode ? config.port : options.port ?? config.port ?? 10100;
|
|
111
|
+
let port: number;
|
|
112
|
+
try {
|
|
113
|
+
port = await findAvailablePort(preferred, "127.0.0.1", {
|
|
114
|
+
preferRetryMs: options.port === undefined ? 750 : 5_000,
|
|
115
|
+
preferRetryIntervalMs: 50,
|
|
116
|
+
allowEphemeralFallback: linkMode ? false : options.port === undefined,
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (linkMode && error instanceof PortUnavailableError) {
|
|
120
|
+
throw new Error(`link mode needs port ${config.port}; free it or change port`, { cause: error });
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
94
124
|
const server = startMachineListener(port, { state: state.value });
|
|
95
125
|
const boundPort = server.port ?? port;
|
|
96
126
|
activeServer = server;
|
|
97
127
|
activePort = boundPort;
|
|
128
|
+
const supervisor = linkMode && existsSync(clientLinkStatePath())
|
|
129
|
+
? createClientLinkSupervisor({
|
|
130
|
+
onLinkEnded: () => scheduleStandaloneRecycle(state.value.tokenFingerprint),
|
|
131
|
+
})
|
|
132
|
+
: null;
|
|
133
|
+
activeSupervisor = supervisor;
|
|
134
|
+
supervisor?.start();
|
|
98
135
|
installCrashGuards();
|
|
99
136
|
writePid(process.pid);
|
|
100
137
|
writeRuntimePort({ pid: process.pid, port: boundPort, hostname: "127.0.0.1" });
|
|
@@ -103,10 +140,22 @@ export async function startClientRuntime(
|
|
|
103
140
|
const shutdown = () => {
|
|
104
141
|
if (shuttingDown) return;
|
|
105
142
|
shuttingDown = true;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
143
|
+
void (async () => {
|
|
144
|
+
try {
|
|
145
|
+
await supervisor?.stop();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.warn(`[client] link supervisor stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
}
|
|
149
|
+
activeSupervisor = null;
|
|
150
|
+
try {
|
|
151
|
+
server.stop(true);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn(`[client] listener stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
154
|
+
} finally {
|
|
155
|
+
cleanup();
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
110
159
|
};
|
|
111
160
|
process.on("SIGINT", shutdown);
|
|
112
161
|
process.on("SIGTERM", shutdown);
|
package/src/client/state.ts
CHANGED
|
@@ -26,6 +26,10 @@ export type ClientConnectionState =
|
|
|
26
26
|
| { kind: "invalid"; reason: string }
|
|
27
27
|
| { kind: "mismatched"; reason: string };
|
|
28
28
|
|
|
29
|
+
export function isLinkConnection(c: OcxClientConnectionConfig | undefined): boolean {
|
|
30
|
+
return c?.transport === "link";
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
const pendingConnectPath = (): string => join(getConfigDir(), "client-connect-pending");
|
|
30
34
|
|
|
31
35
|
/** Validate pending ownership; an optional fingerprint restricts it to that exact key. */
|
|
@@ -783,6 +783,10 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
783
783
|
}
|
|
784
784
|
const slug = String(entry.slug);
|
|
785
785
|
if (!isOcxAuthoredRoutedEntry(entry) || isNativeAliasCatalogEntry(entry)) continue;
|
|
786
|
+
// The builder no longer copies a template's comp_hash onto routed rows (#5796), but a row
|
|
787
|
+
// kept from disk may still carry one. Custom rows, Codex-forward aliases included, never
|
|
788
|
+
// reach this loop: they are rebuilt from config.
|
|
789
|
+
entry.comp_hash = "opencodex";
|
|
786
790
|
const featuredRank = featuredRankOf(slug);
|
|
787
791
|
entry.priority = featuredRank !== undefined
|
|
788
792
|
? featuredRank * priorityStride
|
|
@@ -144,6 +144,11 @@ export function deriveEntry(
|
|
|
144
144
|
delete e.context_window;
|
|
145
145
|
delete e.max_context_window;
|
|
146
146
|
delete e.auto_compact_token_limit;
|
|
147
|
+
// Nor its comp_hash (#5796). Codex compacts a thread whenever the recorded value
|
|
148
|
+
// changes, and the template is whichever native row a rebuild found first, so an
|
|
149
|
+
// inherited value moves with rebuild order. Left unset, normalization gives every
|
|
150
|
+
// routed row the same "opencodex" marker.
|
|
151
|
+
delete e.comp_hash;
|
|
147
152
|
}
|
|
148
153
|
if (typeof e.base_instructions === "string") {
|
|
149
154
|
// Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
|
|
@@ -36,6 +36,7 @@ import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget"
|
|
|
36
36
|
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
37
37
|
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
38
38
|
import { NATIVE_GPT6_ASTRA_MODEL } from "./native-models";
|
|
39
|
+
import { recordOwnedConfigPath } from "../../lib/config-ownership";
|
|
39
40
|
|
|
40
41
|
export function legacyCatalogBackupPath(): string {
|
|
41
42
|
return join(getConfigDir(), "catalog-backup.json");
|
|
@@ -969,14 +970,17 @@ export function catalogHasRoutedEntries(catalog: RawCatalog | null): boolean {
|
|
|
969
970
|
}
|
|
970
971
|
|
|
971
972
|
export function writePristineCatalogBackup(backupPath: string, catalogPath: string, catalog: RawCatalog): void {
|
|
973
|
+
// An existing name is not evidence of ownership; keep pre-ledger/user backups unclaimed.
|
|
972
974
|
if (existsSync(backupPath)) return;
|
|
973
975
|
const onDisk = readCatalog(catalogPath);
|
|
974
976
|
if (onDisk && !catalogHasRoutedEntries(onDisk)) {
|
|
975
977
|
copyFileSync(catalogPath, backupPath);
|
|
978
|
+
recordOwnedConfigPath(getConfigDir(), backupPath);
|
|
976
979
|
return;
|
|
977
980
|
}
|
|
978
981
|
if (!catalogHasRoutedEntries(catalog)) {
|
|
979
982
|
atomicWriteFile(backupPath, JSON.stringify(catalog, null, 2) + "\n");
|
|
983
|
+
recordOwnedConfigPath(getConfigDir(), backupPath);
|
|
980
984
|
}
|
|
981
985
|
}
|
|
982
986
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { initializeConfigOwnership } from "../../lib/config-ownership";
|
|
4
|
+
import { getConfigDir, loadConfig, websocketsEnabled } from "../../config";
|
|
4
5
|
import { shouldSyncCodexOnStart } from "../desired-state";
|
|
5
6
|
import { legacyCustomModelCatalogSlugs } from "../custom-model-catalog-migration";
|
|
6
7
|
import { getCodexHome } from "../paths";
|
|
@@ -289,6 +290,8 @@ function writeRetainedCatalogSync({
|
|
|
289
290
|
// (later syncs would otherwise overwrite it with featured-modified priorities).
|
|
290
291
|
const pristine = pristineCatalogBytes(read);
|
|
291
292
|
if (pristine !== null) {
|
|
293
|
+
// Initialize metadata while the root is empty; never pre-claim the hashed path.
|
|
294
|
+
initializeConfigOwnership(getConfigDir());
|
|
292
295
|
publishHashedCodexCatalogBackup(permit, owningCodexHome, {
|
|
293
296
|
path: catalogBackupPathFor(catalogPath),
|
|
294
297
|
content: pristine,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
2
|
import { shouldSyncCodexOnStart } from "./desired-state";
|
|
3
|
+
import { tomlString } from "./paths";
|
|
3
4
|
import {
|
|
4
5
|
isEffectiveCodexClientCompaction,
|
|
5
6
|
isEffectiveCodexDesktopAuthless,
|
|
@@ -11,7 +12,7 @@ export type CodexDesktopSwitchInertReason =
|
|
|
11
12
|
|
|
12
13
|
export interface CodexDesktopSwitchState {
|
|
13
14
|
stored: boolean;
|
|
14
|
-
effective: boolean;
|
|
15
|
+
effective: boolean | null;
|
|
15
16
|
inertReason?: CodexDesktopSwitchInertReason;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -19,6 +20,8 @@ export type CodexDesktopSwitchApplyReason =
|
|
|
19
20
|
| "not_requested"
|
|
20
21
|
| "proxy_not_running"
|
|
21
22
|
| "integration_disabled"
|
|
23
|
+
| "external_provider"
|
|
24
|
+
| "ownership_undetermined"
|
|
22
25
|
| "write_lock_busy"
|
|
23
26
|
| "injection_refused";
|
|
24
27
|
|
|
@@ -35,7 +38,7 @@ export interface CodexDesktopSwitchReport {
|
|
|
35
38
|
codexDesktopAuthless: CodexDesktopSwitchState;
|
|
36
39
|
codexClientCompaction: CodexDesktopSwitchState;
|
|
37
40
|
apply: CodexDesktopSwitchApply;
|
|
38
|
-
authSource: { presentsCodexAccount: boolean; summary: string };
|
|
41
|
+
authSource: { presentsCodexAccount: boolean | null; summary: string };
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
type DesktopSwitchConfig = Pick<
|
|
@@ -50,9 +53,10 @@ type DesktopSwitchConfig = Pick<
|
|
|
50
53
|
|
|
51
54
|
function describeSwitch(
|
|
52
55
|
stored: boolean,
|
|
53
|
-
effective: boolean,
|
|
56
|
+
effective: boolean | null,
|
|
54
57
|
config: Pick<OcxConfig, "runtimeRole">,
|
|
55
58
|
): CodexDesktopSwitchState {
|
|
59
|
+
if (effective === null) return { stored, effective };
|
|
56
60
|
if (!stored || effective) return { stored, effective };
|
|
57
61
|
return {
|
|
58
62
|
stored,
|
|
@@ -68,15 +72,31 @@ export function describeCodexDesktopSwitches(
|
|
|
68
72
|
apply: CodexDesktopSwitchApply,
|
|
69
73
|
): CodexDesktopSwitchReport {
|
|
70
74
|
const authlessStored = config.codexDesktopAuthless === true;
|
|
71
|
-
const
|
|
75
|
+
const externallyOwned = !apply.applied && apply.reason === "external_provider";
|
|
76
|
+
const ownershipUndetermined = !apply.applied && apply.reason === "ownership_undetermined";
|
|
77
|
+
// An unreadable config.toml leaves ownership undetermined, so the local effective values are
|
|
78
|
+
// withheld exactly like the externally owned case: reporting them would present OpenCodex's
|
|
79
|
+
// stored-versus-computed state as live while the file may belong to another provider.
|
|
80
|
+
const effectiveWithheld = externallyOwned || ownershipUndetermined;
|
|
81
|
+
const authlessEffective = effectiveWithheld ? null : isEffectiveCodexDesktopAuthless(config);
|
|
72
82
|
const compactionStored = config.codexClientCompaction === true;
|
|
73
|
-
const compactionEffective = isEffectiveCodexClientCompaction(config);
|
|
83
|
+
const compactionEffective = effectiveWithheld ? null : isEffectiveCodexClientCompaction(config);
|
|
74
84
|
|
|
75
85
|
return {
|
|
76
86
|
codexDesktopAuthless: describeSwitch(authlessStored, authlessEffective, config),
|
|
77
87
|
codexClientCompaction: describeSwitch(compactionStored, compactionEffective, config),
|
|
78
88
|
apply,
|
|
79
|
-
authSource:
|
|
89
|
+
authSource: externallyOwned
|
|
90
|
+
? {
|
|
91
|
+
presentsCodexAccount: null,
|
|
92
|
+
summary: "An external model provider owns Codex sign-in behavior; its account requirement was not changed.",
|
|
93
|
+
}
|
|
94
|
+
: ownershipUndetermined
|
|
95
|
+
? {
|
|
96
|
+
presentsCodexAccount: null,
|
|
97
|
+
summary: "Whether the Codex app requires its own account sign-in is undetermined; config.toml ownership could not be read.",
|
|
98
|
+
}
|
|
99
|
+
: authlessEffective
|
|
80
100
|
? {
|
|
81
101
|
presentsCodexAccount: false,
|
|
82
102
|
summary: "The Codex app will not require its own account sign-in.",
|
|
@@ -88,6 +108,53 @@ export function describeCodexDesktopSwitches(
|
|
|
88
108
|
};
|
|
89
109
|
}
|
|
90
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The apply record for a report that attempted no rewrite. `not_requested` alone would have
|
|
113
|
+
* the report claiming OpenCodex's stored-versus-effective state as live, so the read path
|
|
114
|
+
* consults the same ownership predicate the injector does and reports external ownership
|
|
115
|
+
* instead — a settings GET and a switch-free PUT then agree with an attempted apply.
|
|
116
|
+
*/
|
|
117
|
+
export async function observedCodexDesktopSwitchApply(): Promise<CodexDesktopSwitchApply> {
|
|
118
|
+
// Same lazy boundary as applyCodexConfigInjection: the ownership predicate lives in the
|
|
119
|
+
// injection graph, which the settings read path must not pull in at module scope.
|
|
120
|
+
const { currentExternalCodexModelProvider } = await import("./inject/config-toml");
|
|
121
|
+
let provider: string | null;
|
|
122
|
+
try {
|
|
123
|
+
provider = currentExternalCodexModelProvider();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// A present-but-unreadable config.toml (permissions, deletion racing existsSync)
|
|
126
|
+
// must not take down the whole settings report. The undetermined reason keeps the
|
|
127
|
+
// reporting contract honest: effective values and the sign-in answer stay null instead
|
|
128
|
+
// of presenting local state a foreign provider may still control.
|
|
129
|
+
return {
|
|
130
|
+
applied: false,
|
|
131
|
+
reason: "ownership_undetermined",
|
|
132
|
+
retryable: true,
|
|
133
|
+
detail: `config.toml ownership could not be determined: ${error instanceof Error ? error.message : String(error)}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!provider) return { applied: false, reason: "not_requested", retryable: false };
|
|
137
|
+
return {
|
|
138
|
+
applied: false,
|
|
139
|
+
reason: "external_provider",
|
|
140
|
+
retryable: false,
|
|
141
|
+
detail: `config.toml selects the external model_provider ${tomlString(provider)}.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The apply gates skip the injector entirely, so they run the same ownership read the
|
|
146
|
+
// observed path does — a disabled integration or an absent runtime must not make a
|
|
147
|
+
// switch PUT report local state the external provider still controls. An undetermined
|
|
148
|
+
// read is kept for the same reason: replacing it with the gate's reason would drop the
|
|
149
|
+
// "ownership could not be determined" explanation the locked save still owes.
|
|
150
|
+
async function observedOwnershipApply(): Promise<CodexDesktopSwitchApply | null> {
|
|
151
|
+
const ownership = await observedCodexDesktopSwitchApply();
|
|
152
|
+
return !ownership.applied
|
|
153
|
+
&& (ownership.reason === "external_provider" || ownership.reason === "ownership_undetermined")
|
|
154
|
+
? ownership
|
|
155
|
+
: null;
|
|
156
|
+
}
|
|
157
|
+
|
|
91
158
|
/**
|
|
92
159
|
* Re-run the Codex config injection so a setting that lives in `~/.codex/config.toml` follows the
|
|
93
160
|
* stored config NOW rather than at the next `ocx sync`.
|
|
@@ -100,13 +167,15 @@ export async function applyCodexConfigInjection(
|
|
|
100
167
|
config: OcxConfig,
|
|
101
168
|
): Promise<CodexDesktopSwitchApply> {
|
|
102
169
|
if (!shouldSyncCodexOnStart(config)) {
|
|
103
|
-
return
|
|
170
|
+
return (await observedOwnershipApply())
|
|
171
|
+
?? { applied: false, reason: "integration_disabled", retryable: false };
|
|
104
172
|
}
|
|
105
173
|
|
|
106
174
|
const { readRuntimePort } = await import("../config/process-state");
|
|
107
175
|
const runtime = readRuntimePort(process.pid);
|
|
108
176
|
if (!runtime) {
|
|
109
|
-
return
|
|
177
|
+
return (await observedOwnershipApply())
|
|
178
|
+
?? { applied: false, reason: "proxy_not_running", retryable: true };
|
|
110
179
|
}
|
|
111
180
|
|
|
112
181
|
try {
|
|
@@ -123,6 +192,14 @@ export async function applyCodexConfigInjection(
|
|
|
123
192
|
detail: result.message,
|
|
124
193
|
};
|
|
125
194
|
}
|
|
195
|
+
if (result.success && result.configApplied === false) {
|
|
196
|
+
return {
|
|
197
|
+
applied: false,
|
|
198
|
+
reason: "external_provider",
|
|
199
|
+
retryable: false,
|
|
200
|
+
detail: result.message,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
126
203
|
if (result.success) {
|
|
127
204
|
// history_paginated_requires_native_writer stands down only the legacy relabel;
|
|
128
205
|
// apply still writes the routing and catalog half for paginated Codex homes.
|
|
@@ -143,6 +220,9 @@ export async function applyCodexConfigInjection(
|
|
|
143
220
|
detail: result.message,
|
|
144
221
|
};
|
|
145
222
|
} catch (error) {
|
|
223
|
+
// The injector may fail its ownership read after the initial apply gates passed.
|
|
224
|
+
const ownership = await observedOwnershipApply();
|
|
225
|
+
if (ownership) return ownership;
|
|
146
226
|
return {
|
|
147
227
|
applied: false,
|
|
148
228
|
reason: "injection_refused",
|
package/src/codex/inject/plan.ts
CHANGED
|
@@ -102,6 +102,19 @@ export type CodexInjectionPlan =
|
|
|
102
102
|
}
|
|
103
103
|
| CodexInjectionPlanOk;
|
|
104
104
|
|
|
105
|
+
function websocketsForRoutingTarget(
|
|
106
|
+
config: OcxConfig | undefined,
|
|
107
|
+
routingTarget: CodexRoutingTarget,
|
|
108
|
+
): boolean {
|
|
109
|
+
// A link client reaches the hub through an HTTP-only tunnel. Its local Codex target is
|
|
110
|
+
// deliberately distinct from the tunnel origin, so force the injected websocket setting off
|
|
111
|
+
// even when the operator enabled the global websocket option. Hub mode can also use a local
|
|
112
|
+
// HTTP target with admission, so the explicit discriminator is required here.
|
|
113
|
+
return (routingTarget as CodexRoutingTarget & { link?: boolean }).link === true
|
|
114
|
+
? false
|
|
115
|
+
: websocketsEnabled(config ?? {});
|
|
116
|
+
}
|
|
117
|
+
|
|
105
118
|
export function deriveCodexInjectionPlan(
|
|
106
119
|
source: string,
|
|
107
120
|
ctx: CodexInjectionPlanContext,
|
|
@@ -240,7 +253,7 @@ export function deriveCodexInjectionPlan(
|
|
|
240
253
|
content =
|
|
241
254
|
content.trimEnd() +
|
|
242
255
|
"\n" +
|
|
243
|
-
buildProviderTableBlockForTarget(routingTarget,
|
|
256
|
+
buildProviderTableBlockForTarget(routingTarget, websocketsForRoutingTarget(config, routingTarget), config?.codexProviderDisplayName);
|
|
244
257
|
// 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
|
|
245
258
|
// are the Design B ones: a user's own root line is never replaced.
|
|
246
259
|
if (keepRootOverrideAlongsideTable) {
|
|
@@ -299,7 +312,7 @@ export function deriveCodexInjectionPlan(
|
|
|
299
312
|
const profileContent = buildProfileFileForTarget(
|
|
300
313
|
routingTarget,
|
|
301
314
|
catalogPath,
|
|
302
|
-
|
|
315
|
+
websocketsForRoutingTarget(config, routingTarget),
|
|
303
316
|
config?.fastMode,
|
|
304
317
|
config?.codexProviderDisplayName,
|
|
305
318
|
);
|
|
@@ -358,7 +371,7 @@ export function deriveCodexInjectionPlan(
|
|
|
358
371
|
*/
|
|
359
372
|
if (hadOcxProviderTableOnDisk && !providerTableMode) {
|
|
360
373
|
content = applyEol(
|
|
361
|
-
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget,
|
|
374
|
+
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsForRoutingTarget(config, routingTarget), config?.codexProviderDisplayName),
|
|
362
375
|
eol,
|
|
363
376
|
);
|
|
364
377
|
}
|
package/src/codex/inject.ts
CHANGED
|
@@ -133,6 +133,8 @@ function runClientWriteGuard(guard: InjectCodexOptions["beforeClientWrite"]): vo
|
|
|
133
133
|
export interface CodexInjectResult {
|
|
134
134
|
success: boolean;
|
|
135
135
|
message: string;
|
|
136
|
+
/** False when injection intentionally preserves configuration owned by another provider. */
|
|
137
|
+
configApplied?: false;
|
|
136
138
|
/**
|
|
137
139
|
* Structured read-only history preflight refusal; never parsed from display text.
|
|
138
140
|
*
|
|
@@ -240,6 +242,7 @@ async function injectCodexConfigImpl(
|
|
|
240
242
|
: undefined;
|
|
241
243
|
return {
|
|
242
244
|
success: true,
|
|
245
|
+
configApplied: false,
|
|
243
246
|
...(nativeSubagentDefaultsWarning
|
|
244
247
|
? { nativeSubagentDefaultsWarning }
|
|
245
248
|
: {}),
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
AtomicWriteResidualTempError,
|
|
6
6
|
AtomicWriteSecretResidualError,
|
|
7
7
|
atomicWriteFile,
|
|
8
|
+
getConfigDir,
|
|
8
9
|
resolveWriteTarget,
|
|
9
10
|
type AtomicWriteIO,
|
|
10
11
|
} from "../../config";
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
hardenSecretPath,
|
|
18
19
|
} from "../../lib/windows-secret-acl";
|
|
19
20
|
import { resetCodexAppServerCatalogStateCache } from "../app-server-processes";
|
|
21
|
+
import { recordOwnedConfigPath } from "../../lib/config-ownership";
|
|
20
22
|
|
|
21
23
|
export interface PreparedCatalogFileWrite {
|
|
22
24
|
readonly path: string;
|
|
@@ -169,6 +171,7 @@ function scrubAndRemoveUnpublishedTemp(
|
|
|
169
171
|
function publishCatalogBackup(
|
|
170
172
|
prepared: PreparedCatalogFileWrite,
|
|
171
173
|
suppliedIo?: CatalogBackupWriteIO,
|
|
174
|
+
onPublished?: () => void,
|
|
172
175
|
): CatalogBackupPublication {
|
|
173
176
|
const io = suppliedIo ?? defaultBackupWriteIO(prepared.path);
|
|
174
177
|
const target = io.resolveTarget(prepared.path);
|
|
@@ -187,7 +190,12 @@ function publishCatalogBackup(
|
|
|
187
190
|
throw error;
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
|
|
193
|
+
try {
|
|
194
|
+
// Publication already succeeded; cleanup failure must not erase that ownership.
|
|
195
|
+
onPublished?.();
|
|
196
|
+
} finally {
|
|
197
|
+
removePublishedTemp(tempPath, io);
|
|
198
|
+
}
|
|
191
199
|
return "written";
|
|
192
200
|
}
|
|
193
201
|
|
|
@@ -211,7 +219,10 @@ export function publishHashedCodexCatalogBackup(
|
|
|
211
219
|
io?: CatalogBackupWriteIO,
|
|
212
220
|
): CatalogBackupPublication {
|
|
213
221
|
assertCatalogWritePermit(permit, owningCodexHome);
|
|
214
|
-
return publishCatalogBackup(prepared, io)
|
|
222
|
+
return publishCatalogBackup(prepared, io, io ? undefined : () => {
|
|
223
|
+
// Called only after a new no-replace publication, never for preserved winners.
|
|
224
|
+
recordOwnedConfigPath(getConfigDir(), prepared.path);
|
|
225
|
+
});
|
|
215
226
|
}
|
|
216
227
|
|
|
217
228
|
/** Atomically publish the legacy immutable backup without clobbering. */
|
package/src/combos/failover.ts
CHANGED
|
@@ -215,11 +215,11 @@ export function coolComboTarget(
|
|
|
215
215
|
code?: string | null;
|
|
216
216
|
message?: string;
|
|
217
217
|
},
|
|
218
|
-
):
|
|
218
|
+
): boolean {
|
|
219
219
|
const now = options?.now ?? Date.now();
|
|
220
220
|
const writerGeneration = options?.writerGeneration ?? captureConfigGeneration();
|
|
221
221
|
const ownerKey = `${comboId}::${targetKey(target)}`;
|
|
222
|
-
if (writerGeneration < lastReconciledGeneration && !liveComboTargets.has(ownerKey)) return;
|
|
222
|
+
if (writerGeneration < lastReconciledGeneration && !liveComboTargets.has(ownerKey)) return false;
|
|
223
223
|
// A server-provided Retry-After is authoritative, including an immediate `0` directive.
|
|
224
224
|
// A quota reset is the next-most-specific signal (#3256); configured and default cooldowns
|
|
225
225
|
// are only fallbacks when upstream supplied neither usable value.
|
|
@@ -240,6 +240,7 @@ export function coolComboTarget(
|
|
|
240
240
|
cooldownUntil: now + (serverDelayMs ?? Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS)),
|
|
241
241
|
});
|
|
242
242
|
sweepExpiredOnWrite(now);
|
|
243
|
+
return true;
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
export function earliestComboCooldown(
|
package/src/combos/index.ts
CHANGED
|
@@ -56,3 +56,15 @@ export {
|
|
|
56
56
|
resetComboEffortWarningStateForTests,
|
|
57
57
|
} from "./request";
|
|
58
58
|
export { earliestQuotaResetAt, quotaResetRemainingMs } from "./reset-window";
|
|
59
|
+
export {
|
|
60
|
+
buildJevRouteQuestion,
|
|
61
|
+
buildJevState,
|
|
62
|
+
JEV_API_URL,
|
|
63
|
+
JEV_MODEL,
|
|
64
|
+
JEV_PROVIDER_ID,
|
|
65
|
+
parseJevDecision,
|
|
66
|
+
resolveJevDecision,
|
|
67
|
+
type JevCandidate,
|
|
68
|
+
type JevDecision,
|
|
69
|
+
type ResolveJevDecisionOptions,
|
|
70
|
+
} from "./jev";
|