@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, mkdirSync,
|
|
4
|
+
openSync, readSync, unlinkSync, writeFileSync, type Stats,
|
|
5
|
+
} from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { forgetEphemeralSecretPath, hardenSecretDir, hardenSecretPath } from "../../lib/windows-secret-acl";
|
|
8
|
+
|
|
9
|
+
const TOKEN_FILE = "proxy-token";
|
|
10
|
+
const TOKEN_LENGTH = 43; // 32 random bytes encoded as unpadded base64url.
|
|
11
|
+
const TOKEN_MAX_BYTES = TOKEN_LENGTH + 2; // Optional LF or CRLF, never arbitrary whitespace.
|
|
12
|
+
const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
13
|
+
const NONBLOCK = constants.O_NONBLOCK ?? 0;
|
|
14
|
+
|
|
15
|
+
export function claudeInterceptProxyTokenPath(configDir: string): string {
|
|
16
|
+
return join(configDir, "claude-intercept", TOKEN_FILE);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function invalidCredential(): Error {
|
|
20
|
+
return new Error("Claude intercept credential is not a safe, valid owner-controlled file");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertOwnedNode(stat: Stats, directory = false): void {
|
|
24
|
+
if (stat.isSymbolicLink() || !(directory ? stat.isDirectory() : stat.isFile())) throw invalidCredential();
|
|
25
|
+
if (process.platform !== "win32" && typeof process.geteuid === "function" && stat.uid !== process.geteuid()) {
|
|
26
|
+
throw invalidCredential();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertSameNode(path: string, expected: Stats, directory = false): void {
|
|
31
|
+
const current = lstatSync(path);
|
|
32
|
+
assertOwnedNode(current, directory);
|
|
33
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino) throw invalidCredential();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read only through the validated descriptor, with a fixed maximum allocation and no links. */
|
|
37
|
+
function readPinnedToken(configDir: string, harden: boolean): string {
|
|
38
|
+
const dir = join(configDir, "claude-intercept");
|
|
39
|
+
const directory = lstatSync(dir);
|
|
40
|
+
assertOwnedNode(directory, true);
|
|
41
|
+
if (process.platform !== "win32" && (directory.mode & 0o077) !== 0) throw invalidCredential();
|
|
42
|
+
const path = claudeInterceptProxyTokenPath(configDir);
|
|
43
|
+
const entry = lstatSync(path);
|
|
44
|
+
assertOwnedNode(entry);
|
|
45
|
+
if (entry.size < TOKEN_LENGTH || entry.size > TOKEN_MAX_BYTES) throw invalidCredential();
|
|
46
|
+
// NONBLOCK prevents a raced-in FIFO from blocking before fstat can reject it.
|
|
47
|
+
const fd = openSync(path, constants.O_RDONLY | NOFOLLOW | NONBLOCK);
|
|
48
|
+
try {
|
|
49
|
+
const opened = fstatSync(fd);
|
|
50
|
+
assertOwnedNode(opened);
|
|
51
|
+
if (opened.dev !== entry.dev || opened.ino !== entry.ino
|
|
52
|
+
|| opened.size < TOKEN_LENGTH || opened.size > TOKEN_MAX_BYTES) throw invalidCredential();
|
|
53
|
+
const bytes = Buffer.alloc(TOKEN_MAX_BYTES + 1);
|
|
54
|
+
let length = 0;
|
|
55
|
+
while (length < bytes.length) {
|
|
56
|
+
const count = readSync(fd, bytes, length, bytes.length - length, length);
|
|
57
|
+
if (count === 0) break;
|
|
58
|
+
length += count;
|
|
59
|
+
}
|
|
60
|
+
const contents = bytes.subarray(0, length).toString("utf8");
|
|
61
|
+
const token = contents.slice(0, TOKEN_LENGTH);
|
|
62
|
+
const ending = contents.slice(TOKEN_LENGTH);
|
|
63
|
+
if (token.length !== TOKEN_LENGTH || !/^[A-Za-z0-9_-]+$/.test(token)
|
|
64
|
+
|| !["", "\n", "\r\n"].includes(ending)) throw invalidCredential();
|
|
65
|
+
const afterRead = fstatSync(fd);
|
|
66
|
+
if (length !== afterRead.size || afterRead.mtimeMs !== opened.mtimeMs) throw invalidCredential();
|
|
67
|
+
assertSameNode(path, opened);
|
|
68
|
+
assertSameNode(dir, directory, true);
|
|
69
|
+
if (harden) {
|
|
70
|
+
if (process.platform === "win32") hardenSecretPath(path, { required: true });
|
|
71
|
+
else fchmodSync(fd, 0o600); // Never chmod a followed/replaced pathname.
|
|
72
|
+
assertSameNode(path, opened);
|
|
73
|
+
assertSameNode(dir, directory, true);
|
|
74
|
+
} else if (process.platform !== "win32" && (afterRead.mode & 0o077) !== 0) {
|
|
75
|
+
throw invalidCredential();
|
|
76
|
+
}
|
|
77
|
+
return token;
|
|
78
|
+
} finally {
|
|
79
|
+
closeSync(fd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The current usable credential, or null. Inspection and CONNECT admission never create one. */
|
|
84
|
+
export function readClaudeInterceptProxyToken(configDir: string): string | null {
|
|
85
|
+
try {
|
|
86
|
+
return readPinnedToken(configDir, false);
|
|
87
|
+
} catch { // no-excuse-ok: catch -- unusable credentials fail closed without exposing filesystem or token details.
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ensurePrivateDirectory(configDir: string): Stats {
|
|
93
|
+
const dir = join(configDir, "claude-intercept");
|
|
94
|
+
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
95
|
+
try {
|
|
96
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
99
|
+
}
|
|
100
|
+
const entry = lstatSync(dir);
|
|
101
|
+
assertOwnedNode(entry, true);
|
|
102
|
+
if (process.platform === "win32") {
|
|
103
|
+
hardenSecretDir(dir, { required: true });
|
|
104
|
+
} else {
|
|
105
|
+
const fd = openSync(dir, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | NOFOLLOW);
|
|
106
|
+
try {
|
|
107
|
+
const opened = fstatSync(fd);
|
|
108
|
+
assertOwnedNode(opened, true);
|
|
109
|
+
if (entry.dev !== opened.dev || entry.ino !== opened.ino) throw invalidCredential();
|
|
110
|
+
fchmodSync(fd, 0o700);
|
|
111
|
+
} finally {
|
|
112
|
+
closeSync(fd);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
assertSameNode(dir, entry, true);
|
|
116
|
+
return entry;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Create once without replacing a winner, invalid file, symlink, or other existing entry. */
|
|
120
|
+
export function ensureClaudeInterceptProxyToken(configDir: string): string {
|
|
121
|
+
const dir = join(configDir, "claude-intercept");
|
|
122
|
+
const directory = ensurePrivateDirectory(configDir);
|
|
123
|
+
try {
|
|
124
|
+
return readPinnedToken(configDir, true);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
127
|
+
}
|
|
128
|
+
assertSameNode(dir, directory, true);
|
|
129
|
+
const path = claudeInterceptProxyTokenPath(configDir);
|
|
130
|
+
const token = randomBytes(32).toString("base64url");
|
|
131
|
+
const temp = join(dir, `.${TOKEN_FILE}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
132
|
+
const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);
|
|
133
|
+
const created = fstatSync(fd);
|
|
134
|
+
try {
|
|
135
|
+
writeFileSync(fd, `${token}\n`, { encoding: "utf8" });
|
|
136
|
+
if (process.platform !== "win32") fchmodSync(fd, 0o600);
|
|
137
|
+
else hardenSecretPath(temp, { required: true, timeoutMemoKey: path });
|
|
138
|
+
assertSameNode(temp, created);
|
|
139
|
+
assertSameNode(dir, directory, true);
|
|
140
|
+
try {
|
|
141
|
+
linkSync(temp, path);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
144
|
+
// A concurrent winner is validated below. No rename/overwrite fallback is safe here.
|
|
145
|
+
}
|
|
146
|
+
return readPinnedToken(configDir, true);
|
|
147
|
+
} finally {
|
|
148
|
+
// Cleanup cannot turn a committed credential into a failed apply or hide the
|
|
149
|
+
// original publication error. Do not retry close: its fd may already be reused.
|
|
150
|
+
let cleanupFailed = false;
|
|
151
|
+
try { closeSync(fd); } catch { cleanupFailed = true; }
|
|
152
|
+
let removed = false;
|
|
153
|
+
try {
|
|
154
|
+
assertSameNode(temp, created);
|
|
155
|
+
unlinkSync(temp);
|
|
156
|
+
removed = true;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") removed = true;
|
|
159
|
+
else cleanupFailed = true;
|
|
160
|
+
}
|
|
161
|
+
if (removed) forgetEphemeralSecretPath(temp);
|
|
162
|
+
if (cleanupFailed) {
|
|
163
|
+
console.warn("[claude] Credential temporary cleanup incomplete; review retained temporary state.");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -8,6 +8,8 @@ import { claudeInterceptCaCertPath, ensureLocalInterceptCaForStartup, issueLocal
|
|
|
8
8
|
import type { PickerRouteInput } from "./picker-models";
|
|
9
9
|
import { createPickerRuntime, type CreatePickerRuntimeOptions, type PickerRuntime } from "./picker-runtime";
|
|
10
10
|
import type { SecurityRunner } from "./picker-trust";
|
|
11
|
+
import { ensureClaudeInterceptProxyToken, readClaudeInterceptProxyToken } from "./proxy-auth";
|
|
12
|
+
import { buildClaudeInterceptEnv, migrateClaudeInterceptSettings } from "./settings";
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Lifecycle for the Claude intercept pair (CONNECT proxy + TLS listener).
|
|
@@ -126,7 +128,20 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
|
|
|
126
128
|
if (options.requestedPort === 0 && !explicitPort) return null;
|
|
127
129
|
const configDir = options.configDir ?? getConfigDir();
|
|
128
130
|
const ca = await ensureLocalInterceptCaForStartup(configDir);
|
|
131
|
+
const authToken = ensureClaudeInterceptProxyToken(configDir);
|
|
129
132
|
const leaf = issueLocalInterceptLeaf(ca, CLAUDE_INTERCEPT_HOSTS);
|
|
133
|
+
// Refresh an env we already own (e.g. a pre-auth proxy URL left by an upgrade) before the
|
|
134
|
+
// authenticated proxy takes over the port — a plain `ocx start` after an update would
|
|
135
|
+
// otherwise 407 every CONNECT until the next `ocx ensure` or apply. Only `stale` state is
|
|
136
|
+
// rewritten, so installs that never applied first-party are untouched.
|
|
137
|
+
try {
|
|
138
|
+
const proxyPort = claudeInterceptProxyPort(options.config, options.publicPort);
|
|
139
|
+
migrateClaudeInterceptSettings(buildClaudeInterceptEnv(proxyPort, claudeInterceptCaCertPath(configDir), authToken));
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// A skipped rewrite degrades to the pre-migration behaviour and ensure/apply retries it,
|
|
142
|
+
// but silence here leaves upgraded clients hitting 407 with no recorded cause.
|
|
143
|
+
console.warn(`[claude-intercept] settings migration skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
144
|
+
}
|
|
130
145
|
const listener = startClaudeInterceptListener<T>({
|
|
131
146
|
leaf,
|
|
132
147
|
dispatch: options.dispatch,
|
|
@@ -137,6 +152,9 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
|
|
|
137
152
|
try {
|
|
138
153
|
proxy = await startConnectProxy(claudeInterceptProxyPort(options.config, options.publicPort), {
|
|
139
154
|
interceptPort: listener.port!,
|
|
155
|
+
// A real apply may recreate a missing token while this listener remains live.
|
|
156
|
+
// Read current validated authority per CONNECT; absent/invalid means deny, not mint.
|
|
157
|
+
authToken: () => readClaudeInterceptProxyToken(configDir),
|
|
140
158
|
});
|
|
141
159
|
} catch (error) {
|
|
142
160
|
await listener.stop(true);
|
|
@@ -165,6 +183,9 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
|
|
|
165
183
|
try {
|
|
166
184
|
pickerProxy = await startConnectProxy(claudePickerProxyPort(options.config, options.publicPort), {
|
|
167
185
|
interceptPort,
|
|
186
|
+
// No authToken: Desktop's egressProxyUrl cannot present proxy credentials, so this
|
|
187
|
+
// listener stays an unauthenticated loopback relay until the profile format can carry
|
|
188
|
+
// one. The intercept proxy above is the credential-bearing hop.
|
|
168
189
|
// No host list here: the choice below depends on which client opened the tunnel.
|
|
169
190
|
interceptHosts: [],
|
|
170
191
|
selectTunnel: (host, port, request) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync
|
|
1
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { atomicWriteFile } from "../../config/atomic-write";
|
|
3
4
|
import { claudeConfigDir } from "../auth-detect";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -10,7 +11,7 @@ import { claudeConfigDir } from "../auth-detect";
|
|
|
10
11
|
* call. Two keys make every Claude Code process route through the local CONNECT proxy while
|
|
11
12
|
* the app itself stays a first-party install:
|
|
12
13
|
*
|
|
13
|
-
* env.HTTPS_PROXY = http://127.0.0.1:<proxy port>
|
|
14
|
+
* env.HTTPS_PROXY = http://opencodex:<token>@127.0.0.1:<proxy port>
|
|
14
15
|
* env.NODE_EXTRA_CA_CERTS = <configDir>/claude-intercept/ca.pem
|
|
15
16
|
*
|
|
16
17
|
* Ownership is tracked by value, never by a marker key. The CA path is the anchor: it lives
|
|
@@ -26,12 +27,12 @@ export interface ClaudeInterceptEnv {
|
|
|
26
27
|
NODE_EXTRA_CA_CERTS: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export function claudeInterceptProxyUrl(port: number): string {
|
|
30
|
-
return `http://127.0.0.1:${port}`;
|
|
30
|
+
export function claudeInterceptProxyUrl(port: number, authToken: string): string {
|
|
31
|
+
return `http://opencodex:${encodeURIComponent(authToken)}@127.0.0.1:${port}`;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export function buildClaudeInterceptEnv(proxyPort: number, caCertPath: string): ClaudeInterceptEnv {
|
|
34
|
-
return { HTTPS_PROXY: claudeInterceptProxyUrl(proxyPort), NODE_EXTRA_CA_CERTS: caCertPath };
|
|
34
|
+
export function buildClaudeInterceptEnv(proxyPort: number, caCertPath: string, authToken: string): ClaudeInterceptEnv {
|
|
35
|
+
return { HTTPS_PROXY: claudeInterceptProxyUrl(proxyPort, authToken), NODE_EXTRA_CA_CERTS: caCertPath };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export type ClaudeInterceptSettingsState =
|
|
@@ -69,7 +70,7 @@ function envRecord(doc: SettingsDoc): Record<string, unknown> {
|
|
|
69
70
|
|
|
70
71
|
/** Loopback proxy URLs are the only shape opencodex ever writes. */
|
|
71
72
|
export function isClaudeInterceptProxyUrl(value: unknown): value is string {
|
|
72
|
-
return typeof value === "string" && /^http:\/\/127\.0\.0\.1:\d{1,5}\/?$/.test(value.trim());
|
|
73
|
+
return typeof value === "string" && /^http:\/\/(?:opencodex:[^@/]+@)?127\.0\.0\.1:\d{1,5}\/?$/.test(value.trim());
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
function isOwnedCaPath(value: unknown, ownedCaPath: string): value is string {
|
|
@@ -111,9 +112,9 @@ export function inspectClaudeInterceptSettings(
|
|
|
111
112
|
|
|
112
113
|
function writeSettings(path: string, doc: SettingsDoc): void {
|
|
113
114
|
mkdirSync(dirname(path), { recursive: true });
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
// The managed env embeds the proxy token, so the file must stay owner-only:
|
|
116
|
+
// atomicWriteFile applies the real NTFS ACL on Windows where chmod is a no-op.
|
|
117
|
+
atomicWriteFile(path, `${JSON.stringify(doc, null, 2)}\n`);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
export type ClaudeInterceptSettingsWrite =
|
|
@@ -123,7 +124,7 @@ export type ClaudeInterceptSettingsWrite =
|
|
|
123
124
|
/** Capture only the managed keys. Rollback preserves unrelated edits and refuses
|
|
124
125
|
* to overwrite a newer proxy/CA choice made after this apply. */
|
|
125
126
|
export function captureClaudeInterceptSettingsRollback(
|
|
126
|
-
expected: ClaudeInterceptEnv,
|
|
127
|
+
expected: ClaudeInterceptEnv | (() => ClaudeInterceptEnv),
|
|
127
128
|
configDir = claudeConfigDir(),
|
|
128
129
|
): () => boolean {
|
|
129
130
|
const path = settingsPath(configDir);
|
|
@@ -132,10 +133,11 @@ export function captureClaudeInterceptSettingsRollback(
|
|
|
132
133
|
const previous = "doc" in before ? { ...envRecord(before.doc) } : {};
|
|
133
134
|
return () => {
|
|
134
135
|
try {
|
|
136
|
+
const target = typeof expected === "function" ? expected() : expected;
|
|
135
137
|
const current = readSettings(path);
|
|
136
138
|
if (!("doc" in current)) return false;
|
|
137
139
|
const env = envRecord(current.doc);
|
|
138
|
-
if (CLAUDE_INTERCEPT_MANAGED_ENV.some(key => env[key] !==
|
|
140
|
+
if (CLAUDE_INTERCEPT_MANAGED_ENV.some(key => env[key] !== target[key])) return false;
|
|
139
141
|
for (const key of CLAUDE_INTERCEPT_MANAGED_ENV) {
|
|
140
142
|
if (previous[key] === undefined) delete env[key];
|
|
141
143
|
else env[key] = previous[key];
|
|
@@ -168,6 +170,25 @@ export function applyClaudeInterceptSettings(
|
|
|
168
170
|
return { ok: true, changed: true, path };
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Rewrite an env block opencodex already owns when it no longer matches what this run
|
|
175
|
+
* would write — a pre-auth `http://127.0.0.1:<port>` left behind by an upgrade would get
|
|
176
|
+
* a 407 from the now-authenticated proxy until `ocx ensure` or an apply ran. Unlike apply
|
|
177
|
+
* this never creates an absent env: only `stale` (owned) state is rewritten, so the
|
|
178
|
+
* runtime can call it on every start without enabling the integration for anyone else.
|
|
179
|
+
*/
|
|
180
|
+
export function migrateClaudeInterceptSettings(
|
|
181
|
+
env: ClaudeInterceptEnv,
|
|
182
|
+
configDir = claudeConfigDir(),
|
|
183
|
+
): ClaudeInterceptSettingsWrite {
|
|
184
|
+
const path = settingsPath(configDir);
|
|
185
|
+
const state = inspectClaudeInterceptSettings(env, configDir);
|
|
186
|
+
if (state.kind === "unreadable") return { ok: false, reason: "unreadable", path };
|
|
187
|
+
if (state.kind === "foreign") return { ok: false, reason: "foreign_env", path };
|
|
188
|
+
if (state.kind !== "stale") return { ok: true, changed: false, path };
|
|
189
|
+
return applyClaudeInterceptSettings(env, configDir);
|
|
190
|
+
}
|
|
191
|
+
|
|
171
192
|
/** Remove the managed keys, but only the values opencodex owns. */
|
|
172
193
|
export function removeClaudeInterceptSettings(
|
|
173
194
|
ownedCaPath: string,
|
package/src/claude/outbound.ts
CHANGED
|
@@ -33,7 +33,7 @@ function reasoningIdentityDigest(value: string): string {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Fixed-size identity that preserves protocol boundaries without retaining upstream strings. */
|
|
36
|
-
function boundedReasoningIdentity(value: unknown): string {
|
|
36
|
+
export function boundedReasoningIdentity(value: unknown): string {
|
|
37
37
|
if (typeof value === "number") {
|
|
38
38
|
if (Number.isSafeInteger(value) && value >= 0) return `n${value}`;
|
|
39
39
|
if (Number.isFinite(value)) return `d${value}`;
|
|
@@ -104,7 +104,7 @@ export function anthropicUsage(usage: unknown, webSearchRequests = 0): Rec {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function sseFrame(name: string, data: Rec): string {
|
|
107
|
+
export function sseFrame(name: string, data: Rec): string {
|
|
108
108
|
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -161,7 +161,7 @@ export function sanitizeWebSearchInput(input: unknown): Rec {
|
|
|
161
161
|
* input (query/queries) and the web_search_tool_result content (hits, or the error
|
|
162
162
|
* object when the search failed). Shared by the SSE and JSON translation paths.
|
|
163
163
|
*/
|
|
164
|
-
function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultContent: unknown; completed: boolean } {
|
|
164
|
+
export function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultContent: unknown; completed: boolean } {
|
|
165
165
|
const action = isRec(item.action) ? item.action : {};
|
|
166
166
|
const queries = Array.isArray(action.queries)
|
|
167
167
|
? action.queries.filter((q): q is string => typeof q === "string" && q.length > 0)
|
|
@@ -205,7 +205,7 @@ function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultConte
|
|
|
205
205
|
* is not a claim about upstream's tokenizer, and it is not final — `message_delta` carries the
|
|
206
206
|
* authoritative count for every reader that waits for it, exactly as before.
|
|
207
207
|
*/
|
|
208
|
-
function messageSnapshot(model: string, confirmedUsage?: Rec, inputTokenFloor?: number): Rec {
|
|
208
|
+
export function messageSnapshot(model: string, confirmedUsage?: Rec, inputTokenFloor?: number): Rec {
|
|
209
209
|
const usage = confirmedUsage
|
|
210
210
|
?? (typeof inputTokenFloor === "number" && Number.isFinite(inputTokenFloor) && inputTokenFloor > 0
|
|
211
211
|
? { input_tokens: Math.trunc(inputTokenFloor), output_tokens: 0 }
|
|
@@ -222,6 +222,43 @@ function messageSnapshot(model: string, confirmedUsage?: Rec, inputTokenFloor?:
|
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* A Responses incomplete reason as the Anthropic client sees it: an output cap and a content
|
|
227
|
+
* filter are real stop reasons; every other reason is a retryable overload so Claude Code backs
|
|
228
|
+
* off instead of accepting a truncated turn.
|
|
229
|
+
*/
|
|
230
|
+
export function anthropicIncompleteOutcome(
|
|
231
|
+
reason: unknown,
|
|
232
|
+
message: unknown,
|
|
233
|
+
): { stopReason: "max_tokens" | "refusal" } | { failMessage: string } {
|
|
234
|
+
if (reason === "max_output_tokens") return { stopReason: "max_tokens" };
|
|
235
|
+
if (reason === "content_filter") return { stopReason: "refusal" };
|
|
236
|
+
return {
|
|
237
|
+
failMessage: typeof message === "string" && message.trim()
|
|
238
|
+
? message
|
|
239
|
+
: `upstream response was incomplete${typeof reason === "string" ? ` (${reason})` : ""}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The HTTP status a Responses `response.failed` error object maps to on the Anthropic wire.
|
|
245
|
+
* Internal failure envelopes carry the classified {type, code, message} but no numeric status,
|
|
246
|
+
* so it is derived with the same mapping /api/logs uses; a classified 429/401/400 then reaches
|
|
247
|
+
* Claude Code as its real Anthropic error type instead of a retryable overload.
|
|
248
|
+
*/
|
|
249
|
+
export function anthropicFailedStatus(error: Rec, message: string): number {
|
|
250
|
+
const code = typeof error.code === "string" ? error.code : undefined;
|
|
251
|
+
return code === "translation_buffer_limit"
|
|
252
|
+
? 413
|
|
253
|
+
: typeof error.status === "number"
|
|
254
|
+
? error.status
|
|
255
|
+
: httpStatusFromTerminalError({
|
|
256
|
+
type: typeof error.type === "string" ? error.type : undefined,
|
|
257
|
+
code: typeof error.code === "string" ? error.code : null,
|
|
258
|
+
message,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
225
262
|
interface OpenBlock {
|
|
226
263
|
kind: "text" | "thinking" | "tool_use";
|
|
227
264
|
index: number;
|
|
@@ -668,16 +705,9 @@ export function responsesSseToAnthropicSse(
|
|
|
668
705
|
case "response.incomplete": {
|
|
669
706
|
const response = isRec(data.response) ? data.response : {};
|
|
670
707
|
const details = isRec(response.incomplete_details) ? response.incomplete_details : {};
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
finish("refusal", response.usage);
|
|
675
|
-
} else {
|
|
676
|
-
const message = typeof details.message === "string" && details.message.trim()
|
|
677
|
-
? details.message
|
|
678
|
-
: `upstream response was incomplete${typeof details.reason === "string" ? ` (${details.reason})` : ""}`;
|
|
679
|
-
fail(529, message, true);
|
|
680
|
-
}
|
|
708
|
+
const outcome = anthropicIncompleteOutcome(details.reason, details.message);
|
|
709
|
+
if ("stopReason" in outcome) finish(outcome.stopReason, response.usage);
|
|
710
|
+
else fail(529, outcome.failMessage, true);
|
|
681
711
|
break;
|
|
682
712
|
}
|
|
683
713
|
case "response.failed": {
|
|
@@ -688,19 +718,7 @@ export function responsesSseToAnthropicSse(
|
|
|
688
718
|
if (code === "translation_buffer_limit") {
|
|
689
719
|
throw new TranslatorBudgetExceededError("live_transient", TRANSLATOR_MAX_TURN_BYTES);
|
|
690
720
|
}
|
|
691
|
-
const status =
|
|
692
|
-
? 413
|
|
693
|
-
: typeof error.status === "number"
|
|
694
|
-
? error.status
|
|
695
|
-
// Internal response.failed envelopes carry the classified {type, code, message}
|
|
696
|
-
// but no numeric status. Derive it with the same mapping /api/logs uses so a
|
|
697
|
-
// classified 429/401/400 reaches Claude Code as its real Anthropic error type
|
|
698
|
-
// instead of being masked as retryable overload.
|
|
699
|
-
: httpStatusFromTerminalError({
|
|
700
|
-
type: typeof error.type === "string" ? error.type : undefined,
|
|
701
|
-
code: typeof error.code === "string" ? error.code : null,
|
|
702
|
-
message,
|
|
703
|
-
});
|
|
721
|
+
const status = anthropicFailedStatus(error, message);
|
|
704
722
|
// Unclassified status-absent response.failed (relaySseWithFailedTail synthetic
|
|
705
723
|
// tail) still lands on a transient 5xx here — the mid-stream reset shape maps to
|
|
706
724
|
// overloaded_error by design.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "../config";
|
|
2
|
+
import { isReservedCodexAccountWord, reportCodexAccountTargetError, resolveCodexAccountTarget } from "./account-target";
|
|
2
3
|
import { hasPassiveAccountQuota } from "../providers/quota";
|
|
3
4
|
import { closeSync, openSync, readSync } from "node:fs";
|
|
4
5
|
import {
|
|
@@ -39,15 +40,15 @@ const AUTO_NOTE = "auto (no pin — lowest-usage account is selected per request
|
|
|
39
40
|
const EXTENDED_USAGE = `Usage:
|
|
40
41
|
ocx account refresh <provider> [--json]
|
|
41
42
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
42
|
-
ocx account alias <provider> <id|main> <display-name|-> [--json]
|
|
43
|
-
ocx account priority <provider> <id|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
|
|
44
|
-
ocx account pause <provider> <id|main> [--json]
|
|
45
|
-
ocx account resume <provider> <id|main> [--json]
|
|
43
|
+
ocx account alias <provider> <id|alias|main> <display-name|-> [--json]
|
|
44
|
+
ocx account priority <provider> <id|alias|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
|
|
45
|
+
ocx account pause <provider> <id|alias|main> [--json]
|
|
46
|
+
ocx account resume <provider> <id|alias|main> [--json]
|
|
46
47
|
ocx account pause-exhausted <provider> [--json]
|
|
47
48
|
ocx account strategy <provider> [<quota|round-robin|fill-first|reset-first>] [--json]
|
|
48
49
|
ocx account sticky <provider> [<1-100>] [--json]
|
|
49
|
-
ocx account remove <provider> <id|main> --yes [--json]
|
|
50
|
-
ocx account clear-cooldown <provider> <id|main> [--json]
|
|
50
|
+
ocx account remove <provider> <id|alias|main> --yes [--json]
|
|
51
|
+
ocx account clear-cooldown <provider> <id|alias|main> [--json]
|
|
51
52
|
ocx account add-key <provider> [--label <label>] [--json]
|
|
52
53
|
ocx account import <provider> --format <format> (--file <path>|--stdin) [--json]`;
|
|
53
54
|
const PIPE_GUIDANCE = `Pipe the API key on stdin, for example:
|
|
@@ -447,12 +448,24 @@ export async function cmdRemove(args: string[], deps: AccountDeps): Promise<numb
|
|
|
447
448
|
}
|
|
448
449
|
const classified = configAndType(deps, name);
|
|
449
450
|
if ("error" in classified) return wantsJson ? fail(classified.error) : usage(`Error: ${classified.error}`);
|
|
450
|
-
|
|
451
|
-
if (classified.type === "codex" && id === MAIN_ID) return wantsJson
|
|
451
|
+
if (classified.type === "codex" && (requestedId === "main" || requestedId === MAIN_ID)) return wantsJson
|
|
452
452
|
? fail("the main Codex App login cannot be removed")
|
|
453
453
|
: usage("Error: the main Codex App login cannot be removed");
|
|
454
454
|
const baseUrl = await resolveBaseUrl(deps);
|
|
455
455
|
if (!baseUrl) return fail("Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.");
|
|
456
|
+
let id = requestedId;
|
|
457
|
+
if (classified.type === "codex") {
|
|
458
|
+
const target = await resolveCodexAccountTarget(deps, baseUrl, requestedId);
|
|
459
|
+
if ("networkDown" in target) {
|
|
460
|
+
const reason = target.transportError ? ` reason: ${target.transportError}` : "";
|
|
461
|
+
return fail(`Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.${reason}`);
|
|
462
|
+
}
|
|
463
|
+
if ("error" in target) {
|
|
464
|
+
const message = target.kind === "not_found" ? `account or key "${requestedId}" was not found` : target.error;
|
|
465
|
+
return wantsJson ? fail(message) : usage(`Error: ${message}`);
|
|
466
|
+
}
|
|
467
|
+
id = target.id;
|
|
468
|
+
}
|
|
456
469
|
const before = await fetchRows(deps, baseUrl, name, classified.type);
|
|
457
470
|
if (before.networkDown) return fail("Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.");
|
|
458
471
|
if (before.errorJson) return fail(errorText(before.errorJson, `failed to verify ${name} before removal`));
|
|
@@ -633,9 +646,12 @@ export async function cmdClearCooldown(args: string[], deps: AccountDeps): Promi
|
|
|
633
646
|
if (classified.type !== "codex") {
|
|
634
647
|
return usage(`Error: ${name} is not a Codex account pool; cooldown clearing applies to Codex accounts only`);
|
|
635
648
|
}
|
|
636
|
-
const id = requestedId === "main" ? MAIN_ID : requestedId;
|
|
637
649
|
const baseUrl = await resolveBaseUrl(deps);
|
|
638
650
|
if (!baseUrl) return proxyUnreachable();
|
|
651
|
+
const target = await resolveCodexAccountTarget(deps, baseUrl, requestedId);
|
|
652
|
+
if ("networkDown" in target) return proxyUnreachable(target.transportError);
|
|
653
|
+
if ("error" in target) return reportCodexAccountTargetError(target);
|
|
654
|
+
const id = target.id;
|
|
639
655
|
const response = await apiJson(deps, baseUrl, "POST", "/api/codex-auth/accounts/clear-cooldown", { id });
|
|
640
656
|
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
641
657
|
if (response.status !== 200) return apiError(response.json, `failed to clear cooldown for ${requestedId}`, response.status);
|
|
@@ -693,8 +709,6 @@ export async function cmdPriority(args: string[], deps: AccountDeps): Promise<nu
|
|
|
693
709
|
if (classified.type !== "codex") {
|
|
694
710
|
return usage("Error: selection order only applies to the openai Codex account pool");
|
|
695
711
|
}
|
|
696
|
-
const id = requestedId === "main" ? MAIN_ID : requestedId;
|
|
697
|
-
|
|
698
712
|
// Validate before touching the network so a typo never reaches the proxy.
|
|
699
713
|
let priority: number | null | undefined;
|
|
700
714
|
if (requestedPriority !== undefined) {
|
|
@@ -706,6 +720,13 @@ export async function cmdPriority(args: string[], deps: AccountDeps): Promise<nu
|
|
|
706
720
|
|
|
707
721
|
const baseUrl = await resolveBaseUrl(deps);
|
|
708
722
|
if (!baseUrl) return proxyUnreachable();
|
|
723
|
+
const target = await resolveCodexAccountTarget(deps, baseUrl, requestedId);
|
|
724
|
+
if ("networkDown" in target) return proxyUnreachable(target.transportError);
|
|
725
|
+
if ("error" in target) {
|
|
726
|
+
if (target.kind === "not_found" && priority === undefined) return usage(`Error: no ${name} account ${requestedId}`);
|
|
727
|
+
return reportCodexAccountTargetError(target);
|
|
728
|
+
}
|
|
729
|
+
const id = target.id;
|
|
709
730
|
|
|
710
731
|
// No value means "show" — a read must not rewrite what it is reporting.
|
|
711
732
|
if (priority === undefined) {
|
|
@@ -770,10 +791,12 @@ export async function cmdPause(args: string[], deps: AccountDeps, paused: boolea
|
|
|
770
791
|
if (classified.type !== "codex") {
|
|
771
792
|
return usage(`Error: ${verb} applies to the openai Codex account pool`);
|
|
772
793
|
}
|
|
773
|
-
const id = requestedId === "main" ? MAIN_ID : requestedId;
|
|
774
|
-
|
|
775
794
|
const baseUrl = await resolveBaseUrl(deps);
|
|
776
795
|
if (!baseUrl) return proxyUnreachable();
|
|
796
|
+
const target = await resolveCodexAccountTarget(deps, baseUrl, requestedId);
|
|
797
|
+
if ("networkDown" in target) return proxyUnreachable(target.transportError);
|
|
798
|
+
if ("error" in target) return reportCodexAccountTargetError(target);
|
|
799
|
+
const id = target.id;
|
|
777
800
|
|
|
778
801
|
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/accounts/pause", { id, paused });
|
|
779
802
|
// Transport sentinel first: status 0 means the proxy never answered, and comparing it to
|
|
@@ -996,12 +1019,21 @@ export async function cmdAlias(args: string[], deps: AccountDeps): Promise<numbe
|
|
|
996
1019
|
if (!name || !requestedId || requestedAlias === undefined || args.length) return usage();
|
|
997
1020
|
const classified = configAndType(deps, name);
|
|
998
1021
|
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
999
|
-
|
|
1000
|
-
|
|
1022
|
+
if (classified.type === "codex" && (requestedId === "main" || requestedId === MAIN_ID)) {
|
|
1023
|
+
return usage("Error: the main Codex App login cannot be renamed");
|
|
1024
|
+
}
|
|
1001
1025
|
const alias = requestedAlias === "-" ? "" : requestedAlias.trim();
|
|
1002
1026
|
if (alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) return usage("Error: alias must be at most 80 printable characters");
|
|
1027
|
+
if (classified.type === "codex" && isReservedCodexAccountWord(alias)) return usage(`Error: "${alias}" is reserved for \`ocx account use\` and cannot be an alias`);
|
|
1003
1028
|
const baseUrl = await resolveBaseUrl(deps);
|
|
1004
1029
|
if (!baseUrl) return proxyUnreachable();
|
|
1030
|
+
let id = requestedId;
|
|
1031
|
+
if (classified.type === "codex") {
|
|
1032
|
+
const target = await resolveCodexAccountTarget(deps, baseUrl, requestedId);
|
|
1033
|
+
if ("networkDown" in target) return proxyUnreachable(target.transportError);
|
|
1034
|
+
if ("error" in target) return reportCodexAccountTargetError(target);
|
|
1035
|
+
id = target.id;
|
|
1036
|
+
}
|
|
1005
1037
|
const path = classified.type === "codex"
|
|
1006
1038
|
? "/api/codex-auth/accounts/alias"
|
|
1007
1039
|
: classified.type === "oauth"
|