@bitkyc08/opencodex 2.49.0 → 2.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/index-D7BdZpZm.js +115 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +43 -1
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +99 -31
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +50 -13
- package/src/codex/auth-api.ts +156 -28
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +99 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +29 -12
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +184 -12
- package/src/generated/compatibility-version.json +188 -116
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/errors.ts +8 -0
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +20 -1
- package/src/providers/registry.ts +35 -6
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +10 -7
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +12 -6
- package/src/server/hub-state.ts +98 -0
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +149 -8
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/request-log.ts +10 -0
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +76 -5
- package/src/server/responses/compact.ts +28 -11
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +201 -48
- package/src/server/responses/policy-fallback.ts +13 -3
- package/src/server/search.ts +2 -2
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +965 -68
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +93 -4
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-D7BdZpZm.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-BoBRSehJ.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { mapReasoningEffort } from "../../reasoning-effort";
|
|
|
4
4
|
import { buildSystemPrompt } from "../coding-agent/protocol";
|
|
5
5
|
import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps } from "../coding-agent/turn";
|
|
6
6
|
import { QODER_PROFILES, type QoderProfile } from "./profiles";
|
|
7
|
+
import { QoderScaffoldFilter, QODER_SCAFFOLD_ERROR_CODE, qoderScaffoldErrorMessage } from "./scaffold-guard";
|
|
7
8
|
|
|
8
9
|
export type QoderAdapterDeps = CodingAgentDeps;
|
|
9
10
|
|
|
@@ -31,6 +32,73 @@ export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderCo
|
|
|
31
32
|
return args;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Wrap the turn's outbound channel with the scaffolding guard (#4190).
|
|
37
|
+
*
|
|
38
|
+
* The vendor CLI can put its own agent layer into the text channel despite being launched
|
|
39
|
+
* with tools and MCP disabled, and the shared stream-json parser forwards a text delta
|
|
40
|
+
* without inspecting it. This is the last point that is still qoder-specific, so the guard
|
|
41
|
+
* sits here rather than in the parser every coding-agent CLI shares.
|
|
42
|
+
*
|
|
43
|
+
* A terminal event flushes both channels first. The held tail is text the filter could not
|
|
44
|
+
* yet prove was not the start of a marker; dropping it would truncate a legitimate answer,
|
|
45
|
+
* and swallowing an entire response before forwarding a "done" reads downstream as an empty
|
|
46
|
+
* completion rather than as the refusal it is.
|
|
47
|
+
*/
|
|
48
|
+
export function guardQoderScaffolding(emit: (event: AdapterEvent) => void): (event: AdapterEvent) => void {
|
|
49
|
+
const textFilter = new QoderScaffoldFilter();
|
|
50
|
+
const thinkingFilter = new QoderScaffoldFilter();
|
|
51
|
+
let closed = false;
|
|
52
|
+
|
|
53
|
+
const refuse = (reason: string): void => {
|
|
54
|
+
if (closed) return;
|
|
55
|
+
closed = true;
|
|
56
|
+
emit({
|
|
57
|
+
type: "error",
|
|
58
|
+
message: qoderScaffoldErrorMessage(reason),
|
|
59
|
+
status: 502,
|
|
60
|
+
errorType: "upstream_error",
|
|
61
|
+
code: QODER_SCAFFOLD_ERROR_CODE,
|
|
62
|
+
// Intermittent, but a silent retry spends the operator's vendor credits on a
|
|
63
|
+
// contract violation the proxy cannot influence. Surface it instead.
|
|
64
|
+
retryable: false,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (event: AdapterEvent): void => {
|
|
69
|
+
if (closed) return;
|
|
70
|
+
if (event.type === "text_delta") {
|
|
71
|
+
const cleaned = textFilter.push(event.text);
|
|
72
|
+
if (cleaned.text) emit({ ...event, text: cleaned.text });
|
|
73
|
+
if (cleaned.fail) refuse(cleaned.fail);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (event.type === "thinking_delta") {
|
|
77
|
+
const cleaned = thinkingFilter.push(event.thinking);
|
|
78
|
+
if (cleaned.text) emit({ ...event, thinking: cleaned.text });
|
|
79
|
+
if (cleaned.fail) refuse(cleaned.fail);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.type === "done" || event.type === "error" || event.type === "incomplete") {
|
|
83
|
+
const tail = textFilter.flush();
|
|
84
|
+
const reasoning = thinkingFilter.flush();
|
|
85
|
+
if (tail.text) emit({ type: "text_delta", text: tail.text });
|
|
86
|
+
if (reasoning.text) emit({ type: "thinking_delta", thinking: reasoning.text });
|
|
87
|
+
const fail = tail.fail ?? reasoning.fail;
|
|
88
|
+
// A vendor error already carries the better explanation for why the turn ended;
|
|
89
|
+
// only a terminal that claims success is replaced.
|
|
90
|
+
if (fail && event.type !== "error") {
|
|
91
|
+
refuse(fail);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
closed = true;
|
|
95
|
+
emit(event);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
emit(event);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
34
102
|
export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapterDeps = {}): ProviderAdapter {
|
|
35
103
|
return {
|
|
36
104
|
name: "qoder",
|
|
@@ -60,7 +128,7 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
|
|
|
60
128
|
provider,
|
|
61
129
|
parsed,
|
|
62
130
|
incoming,
|
|
63
|
-
emit,
|
|
131
|
+
emit: guardQoderScaffolding(emit),
|
|
64
132
|
buildArgs: (_profile, req, prov) => buildQoderArgs(req, prov),
|
|
65
133
|
buildEnv: (profile, apiKey) => buildQoderChildEnv(profile as QoderProfile, apiKey),
|
|
66
134
|
deps,
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor-scaffolding guard for the qoder route (#4190).
|
|
3
|
+
*
|
|
4
|
+
* The qoder route is contractually a text and reasoning surface: the CLI is spawned with
|
|
5
|
+
* `--tools "" --strict-mcp-config --setting-sources ""`, and Codex keeps tool ownership.
|
|
6
|
+
* The vendor CLI does not always honour that. It has been observed emitting its own agent
|
|
7
|
+
* layer into the assistant text channel — an MCP lazy-loading `<system-reminder>` block
|
|
8
|
+
* listing the local machine's configured MCP servers, and framework tool-call markup with a
|
|
9
|
+
* mismatched closer. Both reached the client verbatim, because the shared stream-json parser
|
|
10
|
+
* forwards a text delta without inspecting it.
|
|
11
|
+
*
|
|
12
|
+
* Two shapes, two answers. A complete `<system-reminder>` block is recognizable and
|
|
13
|
+
* self-delimiting, so it is removed and the surrounding answer survives. Anything else that
|
|
14
|
+
* carries a scaffolding signature is not repairable by guesswork — a partial tool-call block
|
|
15
|
+
* has no reliable end, and the text around it may already be the vendor's own agent
|
|
16
|
+
* narration rather than the model's answer — so the turn fails closed instead.
|
|
17
|
+
*
|
|
18
|
+
* The filter is a stream, not a regex over a finished string: a marker can be split across
|
|
19
|
+
* deltas, so a tail that is still a possible marker prefix is held back rather than emitted.
|
|
20
|
+
* Callers must therefore `flush()` before forwarding a terminal event, or a legitimate
|
|
21
|
+
* answer ending in "<" would lose its last character.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Error code for a turn refused because vendor scaffolding reached the text channel. */
|
|
25
|
+
export const QODER_SCAFFOLD_ERROR_CODE = "vendor_scaffold_detected";
|
|
26
|
+
|
|
27
|
+
const REMINDER_OPEN = "<system-reminder";
|
|
28
|
+
const REMINDER_CLOSE = "</system-reminder>";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Scaffolding signatures that are never repaired.
|
|
32
|
+
*
|
|
33
|
+
* `<functions.` and the invoke pair are the vendor runtime's tool-call markup. The reported
|
|
34
|
+
* leak carried `<functions.exec>` opened and `</invoke>` closed — mismatched, which is what
|
|
35
|
+
* a model emitting remembered markup looks like, and exactly why reconstructing the intended
|
|
36
|
+
* text is not possible. A stray `</system-reminder>` with no opener is in the same class:
|
|
37
|
+
* the block it belonged to was already partly forwarded, or never existed.
|
|
38
|
+
*/
|
|
39
|
+
const UNREPAIRABLE_MARKERS = ["<functions.", "<invoke name=", "<invoke>", "</invoke>", REMINDER_CLOSE] as const;
|
|
40
|
+
|
|
41
|
+
/** Markers that end a block rather than start one; their prefix is never an answer. */
|
|
42
|
+
const CLOSING_MARKERS = new Set<string>(["</invoke>", REMINDER_CLOSE]);
|
|
43
|
+
|
|
44
|
+
/** Every marker the scanner must be able to recognize mid-split. */
|
|
45
|
+
const ALL_MARKERS = [REMINDER_OPEN, ...UNREPAIRABLE_MARKERS] as const;
|
|
46
|
+
|
|
47
|
+
const MAX_MARKER_LENGTH = Math.max(...ALL_MARKERS.map(marker => marker.length));
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* True when `<system-reminder` at `at` is the tag rather than the start of a longer word.
|
|
51
|
+
*
|
|
52
|
+
* The opener is matched without its `>` so a truncated or self-closed tag still suppresses,
|
|
53
|
+
* which means `<system-reminders>` in an ordinary answer would otherwise open a block and
|
|
54
|
+
* refuse the turn. A stem running to the end of the buffer still counts: more text may be
|
|
55
|
+
* arriving, and reading it as prose is the one reading that could release the block body.
|
|
56
|
+
*/
|
|
57
|
+
function reminderOpensHere(lowered: string, at: number): boolean {
|
|
58
|
+
const after = lowered[at + REMINDER_OPEN.length];
|
|
59
|
+
return after === undefined || /[\s/>]/.test(after);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ceiling on a suppressed block before it is treated as unterminated.
|
|
64
|
+
*
|
|
65
|
+
* The block itself is discarded as it arrives, so this is not a memory bound — only the
|
|
66
|
+
* trailing bytes needed to spot a split closer are retained. It bounds how much of a turn a
|
|
67
|
+
* single unclosed reminder is allowed to swallow silently before the turn is refused.
|
|
68
|
+
*/
|
|
69
|
+
const MAX_SUPPRESSED_CHARS = 64 * 1024;
|
|
70
|
+
|
|
71
|
+
/** Result of feeding one chunk: the text safe to forward, and a refusal reason once tripped. */
|
|
72
|
+
export interface ScaffoldFilterResult {
|
|
73
|
+
/** Text cleared for the client. Empty when everything in the chunk was held or dropped. */
|
|
74
|
+
text: string;
|
|
75
|
+
/** Non-null exactly once, on the chunk that trips the guard. */
|
|
76
|
+
fail: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Longest suffix of `text` that could still grow into one of the markers. */
|
|
80
|
+
function heldSuffixLength(text: string): number {
|
|
81
|
+
const limit = Math.min(MAX_MARKER_LENGTH - 1, text.length);
|
|
82
|
+
for (let length = limit; length > 0; length--) {
|
|
83
|
+
const suffix = text.slice(text.length - length).toLowerCase();
|
|
84
|
+
for (const marker of ALL_MARKERS) {
|
|
85
|
+
if (marker.length > length && marker.startsWith(suffix)) return length;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Streaming scaffolding filter for one channel (text or reasoning).
|
|
93
|
+
*
|
|
94
|
+
* One instance per channel: the two never share suppression state, so a reminder block
|
|
95
|
+
* opened in reasoning cannot swallow the answer text.
|
|
96
|
+
*/
|
|
97
|
+
export class QoderScaffoldFilter {
|
|
98
|
+
private mode: "pass" | "suppress" = "pass";
|
|
99
|
+
private pending = "";
|
|
100
|
+
private suppressedTail = "";
|
|
101
|
+
private suppressedChars = 0;
|
|
102
|
+
private failed = false;
|
|
103
|
+
/** True once a reminder block has been suppressed on this channel. */
|
|
104
|
+
private suppressedBlock = false;
|
|
105
|
+
/** Open reminder blocks; only the closer that unwinds the last one ends suppression. */
|
|
106
|
+
private suppressDepth = 0;
|
|
107
|
+
|
|
108
|
+
push(chunk: string): ScaffoldFilterResult {
|
|
109
|
+
if (this.failed || !chunk) return { text: "", fail: null };
|
|
110
|
+
let cleared = "";
|
|
111
|
+
let buffer = this.mode === "pass" ? this.pending + chunk : chunk;
|
|
112
|
+
this.pending = "";
|
|
113
|
+
|
|
114
|
+
for (;;) {
|
|
115
|
+
if (this.mode === "suppress") {
|
|
116
|
+
const scan = this.suppressedTail + buffer;
|
|
117
|
+
const scanned = scan.toLowerCase();
|
|
118
|
+
// Unwind nesting rather than ending at the first closer. A reminder containing another
|
|
119
|
+
// reminder would otherwise hand the outer block's remaining body — the MCP server list
|
|
120
|
+
// in the reported leak — to the client as the model's answer, with a successful
|
|
121
|
+
// terminal and nothing to signal that anything had gone wrong.
|
|
122
|
+
let cursor = 0;
|
|
123
|
+
let close = -1;
|
|
124
|
+
for (;;) {
|
|
125
|
+
const nextClose = scanned.indexOf(REMINDER_CLOSE, cursor);
|
|
126
|
+
if (nextClose < 0) break;
|
|
127
|
+
let nextOpen = scanned.indexOf(REMINDER_OPEN, cursor);
|
|
128
|
+
while (nextOpen >= 0 && !reminderOpensHere(scanned, nextOpen)) {
|
|
129
|
+
nextOpen = scanned.indexOf(REMINDER_OPEN, nextOpen + 1);
|
|
130
|
+
}
|
|
131
|
+
if (nextOpen >= 0 && nextOpen < nextClose) {
|
|
132
|
+
this.suppressDepth += 1;
|
|
133
|
+
cursor = nextOpen + REMINDER_OPEN.length;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
this.suppressDepth -= 1;
|
|
137
|
+
cursor = nextClose + REMINDER_CLOSE.length;
|
|
138
|
+
if (this.suppressDepth === 0) {
|
|
139
|
+
close = nextClose;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (close < 0) {
|
|
144
|
+
this.suppressedChars += buffer.length;
|
|
145
|
+
if (this.suppressedChars > MAX_SUPPRESSED_CHARS) {
|
|
146
|
+
return this.fail(cleared, `an unterminated ${REMINDER_OPEN}> block`);
|
|
147
|
+
}
|
|
148
|
+
// The block is discarded as it arrives; only enough tail to spot a split closer is kept.
|
|
149
|
+
// The tail must cover a split opener too, now that nesting is counted.
|
|
150
|
+
this.suppressedTail = scan.slice(Math.max(0, scan.length - (MAX_MARKER_LENGTH - 1)));
|
|
151
|
+
return { text: cleared, fail: null };
|
|
152
|
+
}
|
|
153
|
+
buffer = scan.slice(close + REMINDER_CLOSE.length);
|
|
154
|
+
this.mode = "pass";
|
|
155
|
+
this.suppressedTail = "";
|
|
156
|
+
this.suppressedChars = 0;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let earliest = -1;
|
|
161
|
+
let found = "";
|
|
162
|
+
const lowered = buffer.toLowerCase();
|
|
163
|
+
for (const marker of ALL_MARKERS) {
|
|
164
|
+
let at = lowered.indexOf(marker);
|
|
165
|
+
while (at >= 0 && marker === REMINDER_OPEN && !reminderOpensHere(lowered, at)) {
|
|
166
|
+
at = lowered.indexOf(marker, at + 1);
|
|
167
|
+
}
|
|
168
|
+
if (at < 0) continue;
|
|
169
|
+
// A closer sitting exactly where an opener starts cannot happen, so ties are impossible.
|
|
170
|
+
if (earliest < 0 || at < earliest) {
|
|
171
|
+
earliest = at;
|
|
172
|
+
found = marker;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (earliest < 0) {
|
|
177
|
+
const held = heldSuffixLength(buffer);
|
|
178
|
+
cleared += held > 0 ? buffer.slice(0, buffer.length - held) : buffer;
|
|
179
|
+
this.pending = held > 0 ? buffer.slice(buffer.length - held) : "";
|
|
180
|
+
return { text: cleared, fail: null };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Text produced before the scaffolding is the model's own answer, and it is kept — but
|
|
184
|
+
// only while this channel has not already suppressed a block. Once it has, the text
|
|
185
|
+
// between that block and an unrepairable marker is not an answer that happens to
|
|
186
|
+
// precede a leak; it is the region the vendor was narrating in, and in the reported
|
|
187
|
+
// case it carries the MCP server list. Forwarding it on the way to a refusal would
|
|
188
|
+
// publish exactly what the refusal exists to contain.
|
|
189
|
+
// A closer with no opener never keeps its prefix either: the block it belonged to was
|
|
190
|
+
// already partly forwarded or never existed, so the text ahead of it is that body.
|
|
191
|
+
if (!CLOSING_MARKERS.has(found) && (found === REMINDER_OPEN || !this.suppressedBlock)) {
|
|
192
|
+
cleared += buffer.slice(0, earliest);
|
|
193
|
+
}
|
|
194
|
+
if (found !== REMINDER_OPEN) return this.fail(cleared, `vendor tool-call markup (${found})`);
|
|
195
|
+
this.suppressedBlock = true;
|
|
196
|
+
this.mode = "suppress";
|
|
197
|
+
this.suppressDepth = 1;
|
|
198
|
+
this.suppressedTail = "";
|
|
199
|
+
this.suppressedChars = 0;
|
|
200
|
+
buffer = buffer.slice(earliest + REMINDER_OPEN.length);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Release the held tail. Call before forwarding a terminal event, never mid-stream. */
|
|
205
|
+
flush(): ScaffoldFilterResult {
|
|
206
|
+
if (this.failed) return { text: "", fail: null };
|
|
207
|
+
if (this.mode === "suppress") return this.fail("", `an unterminated ${REMINDER_OPEN}> block`);
|
|
208
|
+
const text = this.pending;
|
|
209
|
+
this.pending = "";
|
|
210
|
+
return { text, fail: null };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private fail(cleared: string, reason: string): ScaffoldFilterResult {
|
|
214
|
+
this.failed = true;
|
|
215
|
+
this.pending = "";
|
|
216
|
+
this.suppressedTail = "";
|
|
217
|
+
return { text: cleared, fail: reason };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Message for a refused turn.
|
|
223
|
+
*
|
|
224
|
+
* It names the marker class and nothing else. The leaked reminder in the report enumerated
|
|
225
|
+
* the operator's own MCP servers, so echoing the offending text back — into an error the
|
|
226
|
+
* client renders, and that a user may paste into an issue — would publish the thing this
|
|
227
|
+
* guard exists to contain.
|
|
228
|
+
*/
|
|
229
|
+
export function qoderScaffoldErrorMessage(reason: string): string {
|
|
230
|
+
return `Qoder CLI emitted ${reason} in the assistant text channel. This route runs the CLI with`
|
|
231
|
+
+ " its own tools and MCP servers disabled and Codex owns tool control, so the turn was refused"
|
|
232
|
+
+ " rather than forwarding vendor agent scaffolding to the client.";
|
|
233
|
+
}
|
|
@@ -94,7 +94,23 @@ function entryParts(entry: string, config: OcxConfig): { alias: string; id: stri
|
|
|
94
94
|
return { alias: claudeCodeNativeAlias(entry), id: entry, provider: "native" };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
export function buildClaudeAgentDefs(
|
|
97
|
+
export function buildClaudeAgentDefs(
|
|
98
|
+
config: OcxConfig,
|
|
99
|
+
windows: Record<string, number>,
|
|
100
|
+
configDir = claudeConfigDir(),
|
|
101
|
+
/**
|
|
102
|
+
* The roster to generate defs from, overriding local `config.subagentModels` (#4236).
|
|
103
|
+
*
|
|
104
|
+
* A connected client's local roster is whatever it had before it joined the hub — on a fresh
|
|
105
|
+
* client, the five native defaults — while the hub's featured roster is the list that
|
|
106
|
+
* actually routes. Passing it in keeps this function pure and keeps the override visible at
|
|
107
|
+
* the call site instead of hidden behind a config read.
|
|
108
|
+
*
|
|
109
|
+
* Undefined preserves today's behaviour exactly, including "unset means the defaults, an
|
|
110
|
+
* explicit `[]` means none".
|
|
111
|
+
*/
|
|
112
|
+
rosterOverride?: readonly string[],
|
|
113
|
+
): ClaudeAgentDef[] {
|
|
98
114
|
const blockedSkills = effectiveBlockedSkillNames(config.claudeCode);
|
|
99
115
|
const blockedSkillsFor = (model: string): readonly string[] => {
|
|
100
116
|
const unmarked = stripOneMillionMarker(model);
|
|
@@ -137,7 +153,8 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
|
|
|
137
153
|
|
|
138
154
|
// Default roster applies only when the field is UNSET — an explicit [] is
|
|
139
155
|
// respected (audit 071 #6: an upgraded config must not lose the default five).
|
|
140
|
-
const roster =
|
|
156
|
+
const roster = rosterOverride
|
|
157
|
+
?? (config.subagentModels === undefined ? DEFAULT_SUBAGENT_MODELS : config.subagentModels);
|
|
141
158
|
for (const entry of roster.slice(0, 5)) {
|
|
142
159
|
if (typeof entry !== "string" || entry.trim() === "") continue;
|
|
143
160
|
const { alias, id, provider } = entryParts(entry.trim(), config);
|
|
@@ -260,13 +277,20 @@ export function syncClaudeAgentDefs(defs: readonly ClaudeAgentDef[], configDir =
|
|
|
260
277
|
}
|
|
261
278
|
|
|
262
279
|
/** Launch-time hook: gate + build + sync in one call (used by ocx claude and systemEnv). */
|
|
263
|
-
export function injectClaudeAgentDefs(
|
|
280
|
+
export function injectClaudeAgentDefs(
|
|
281
|
+
config: OcxConfig,
|
|
282
|
+
windows: Record<string, number>,
|
|
283
|
+
configDir?: string,
|
|
284
|
+
/** Hub-sourced roster on a connected client; see `buildClaudeAgentDefs`. */
|
|
285
|
+
rosterOverride?: readonly string[],
|
|
286
|
+
): string[] | null {
|
|
264
287
|
if (config.claudeCode?.enabled === false || config.claudeCode?.injectAgents === false) {
|
|
265
288
|
// Disabled: prune verified-owned files so stale definitions stop loading
|
|
266
|
-
// in future sessions (audit 071 #3).
|
|
289
|
+
// in future sessions (audit 071 #3). The roster override is irrelevant here by
|
|
290
|
+
// construction: there is nothing to build.
|
|
267
291
|
return syncClaudeAgentDefs([], configDir);
|
|
268
292
|
}
|
|
269
|
-
return syncClaudeAgentDefs(buildClaudeAgentDefs(config, windows, configDir), configDir);
|
|
293
|
+
return syncClaudeAgentDefs(buildClaudeAgentDefs(config, windows, configDir, rosterOverride), configDir);
|
|
270
294
|
}
|
|
271
295
|
/**
|
|
272
296
|
* Dispatcher directive appended to every ocx-* description. The ocx-route body
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type DesktopProfileModel,
|
|
23
23
|
} from "./desktop-profile";
|
|
24
24
|
import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../codex/catalog";
|
|
25
|
+
import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
|
|
25
26
|
import { assertDesktop3pModelsValid } from "./desktop-3p-guard";
|
|
26
27
|
|
|
27
28
|
export interface Desktop3pModelEntry {
|
|
@@ -322,9 +323,14 @@ export function activeDesktop3pAlias(provider: string, modelId: string): string
|
|
|
322
323
|
* channel for supports1m/tier pins and it overrides discovery anyway (no merge), so
|
|
323
324
|
* discovery stays off for determinism. supports1m makes Desktop offer a separate 1M
|
|
324
325
|
* row; selecting it sends the bare id + `anthropic-beta: context-1m-2025-08-07`.
|
|
326
|
+
*
|
|
327
|
+
* `portOrOrigin` is the LOCAL destination Desktop should dial, already resolved by the caller
|
|
328
|
+
* (see `writeDesktop3pConfig`): on a hub that is the unauthenticated loopback listener, and with
|
|
329
|
+
* no listener the bind address — which is why an ORIGIN is accepted and not only a port. A bare
|
|
330
|
+
* port keeps meaning `http://127.0.0.1:<port>`, so every existing caller and test is unchanged.
|
|
325
331
|
*/
|
|
326
332
|
export function generateDesktop3pConfig(
|
|
327
|
-
|
|
333
|
+
portOrOrigin: number | string,
|
|
328
334
|
nativeSlugs: string[],
|
|
329
335
|
routedModels: Array<Desktop3pRoutedModel>,
|
|
330
336
|
apiKey = "ocx",
|
|
@@ -335,7 +341,7 @@ export function generateDesktop3pConfig(
|
|
|
335
341
|
const base = {
|
|
336
342
|
inferenceProvider: "gateway",
|
|
337
343
|
inferenceCredentialKind: "static",
|
|
338
|
-
inferenceGatewayBaseUrl: `http://127.0.0.1:${
|
|
344
|
+
inferenceGatewayBaseUrl: typeof portOrOrigin === "number" ? `http://127.0.0.1:${portOrOrigin}` : portOrOrigin,
|
|
339
345
|
inferenceGatewayApiKey: apiKey,
|
|
340
346
|
};
|
|
341
347
|
if (mode === "discovery") {
|
|
@@ -620,8 +626,30 @@ export function writeDesktop3pConfig(
|
|
|
620
626
|
if (connection.kind === "connected" || inspectRemoteDesktopCleanup().kind !== "absent") {
|
|
621
627
|
return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_remote_store_active" };
|
|
622
628
|
}
|
|
629
|
+
// Claude Desktop runs on this machine, so it dials the unauthenticated loopback listener
|
|
630
|
+
// when one is enabled — on such a hub that is the only credential-free local socket
|
|
631
|
+
// (#4236) — and otherwise the bind address, which answers but demands data-plane
|
|
632
|
+
// admission. Resolved here, from the config this write already re-read, rather than in
|
|
633
|
+
// the pure generator: `latest.config` is the freshest answer any caller could pass in.
|
|
634
|
+
const destination = localInferenceDestination(latest.config, port);
|
|
635
|
+
// Desktop can carry a credential, so it does: the key the caller passed (the first
|
|
636
|
+
// configured `apiKeys` entry), else the env token / hardened service token file. This is
|
|
637
|
+
// the DATA-PLANE secret only — an admin token must never enter an exported client
|
|
638
|
+
// configuration (reviewer constraint on #4236).
|
|
639
|
+
const gatewayKey = destination.requiresAdmissionToken
|
|
640
|
+
? apiKey ?? localAdmissionToken(latest.config)
|
|
641
|
+
: apiKey;
|
|
642
|
+
if (destination.requiresAdmissionToken && !gatewayKey) {
|
|
643
|
+
// The placeholder the generator defaults to would 401 on this bind. Write the profile
|
|
644
|
+
// anyway — a reachable URL with a visible auth failure beats a dead socket — but say so.
|
|
645
|
+
console.error(
|
|
646
|
+
`⚠ Claude Desktop will dial ${destination.origin}, which requires an opencodex data-plane `
|
|
647
|
+
+ "credential that could not be resolved. Configure an API key or enable "
|
|
648
|
+
+ "`unauthenticatedLoopbackListener`.",
|
|
649
|
+
);
|
|
650
|
+
}
|
|
623
651
|
return writeDesktop3pConfigWithGenerator(() => (
|
|
624
|
-
generateDesktop3pConfig(
|
|
652
|
+
generateDesktop3pConfig(destination.origin, nativeSlugs, routedModels, gatewayKey, mode, profile, nativeContextCap)
|
|
625
653
|
));
|
|
626
654
|
}), lifecycleLockDeps);
|
|
627
655
|
} catch { return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_lifecycle_busy_or_unsafe" }; }
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import {
|
|
16
|
+
import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
|
|
17
17
|
import type { OcxConfig } from "../types";
|
|
18
18
|
|
|
19
19
|
export interface GatewayModelRow {
|
|
@@ -24,7 +24,12 @@ export interface GatewayModelRow {
|
|
|
24
24
|
export interface GatewayModelCacheRefreshOptions {
|
|
25
25
|
timeoutMs?: number;
|
|
26
26
|
configDir?: string;
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Admission credential source AND local destination source: the cache file's `baseUrl` must
|
|
29
|
+
* equal the `ANTHROPIC_BASE_URL` the CLI is launched with or Claude Code ignores the whole
|
|
30
|
+
* cache, so this has to resolve the same loopback listener `buildClaudeEnv` resolves (#4236).
|
|
31
|
+
*/
|
|
32
|
+
admissionConfig?: Pick<OcxConfig, "apiKeys" | "hostname" | "unauthenticatedLoopbackListener">;
|
|
28
33
|
env?: NodeJS.ProcessEnv;
|
|
29
34
|
fetchImpl?: typeof fetch;
|
|
30
35
|
}
|
|
@@ -60,19 +65,6 @@ export function writeGatewayModelCache(baseUrl: string, models: readonly Gateway
|
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
/**
|
|
64
|
-
* Hardened service-token file, the same precedence `ocx opencode` uses. A service
|
|
65
|
-
* install writes the admission token to disk rather than the interactive environment,
|
|
66
|
-
* so an interactive `ocx claude` with neither env token nor configured key would
|
|
67
|
-
* otherwise still get a 401 and keep a stale picker list.
|
|
68
|
-
*/
|
|
69
|
-
function serviceFileToken(env: NodeJS.ProcessEnv): string | null {
|
|
70
|
-
const lookup = env.OCX_API_TOKEN_FILE?.trim()
|
|
71
|
-
? env
|
|
72
|
-
: { ...env, OCX_API_TOKEN_FILE: serviceApiTokenFilePath() };
|
|
73
|
-
return loadServiceTokenFromFile(lookup as Record<string, string | undefined>);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
68
|
/** Fetch the anthropic-flavor /v1/models from the local proxy and write the cache. */
|
|
77
69
|
export async function refreshGatewayModelCacheFromProxy(
|
|
78
70
|
port: number,
|
|
@@ -92,17 +84,16 @@ export async function refreshGatewayModelCacheFromProxy(
|
|
|
92
84
|
// request sent to its local 127.0.0.1 address. Reuse the same dedicated
|
|
93
85
|
// credential domain as /v1/models admission; never place it in Authorization,
|
|
94
86
|
// which can belong to an upstream provider on other data-plane surfaces.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
?.key.trim();
|
|
87
|
+
// Env token, then the hardened service token file (a service install writes the admission
|
|
88
|
+
// token to disk rather than the interactive environment), then a configured key — one
|
|
89
|
+
// shared ladder, so this cannot drift from what `buildClaudeEnv` puts in the launch env.
|
|
99
90
|
const admissionToken = typeof portOrTarget === "number"
|
|
100
|
-
?
|
|
91
|
+
? localAdmissionToken(options.admissionConfig, options.env ?? process.env)
|
|
101
92
|
: portOrTarget.admissionToken;
|
|
102
93
|
if (admissionToken) headers.set("x-opencodex-api-key", admissionToken);
|
|
103
94
|
|
|
104
95
|
const baseUrl = typeof portOrTarget === "number"
|
|
105
|
-
?
|
|
96
|
+
? localInferenceDestination(options.admissionConfig, portOrTarget).origin
|
|
106
97
|
: new URL(portOrTarget.baseUrl).origin;
|
|
107
98
|
|
|
108
99
|
// ?ids=cli pins the readable claude-ocx id family deterministically (audit 051
|
package/src/claude/inbound.ts
CHANGED
|
@@ -150,10 +150,17 @@ function blockedSkillCallIds(messages: readonly unknown[], blocked: readonly str
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Claude Code (observed 2026-07-11, real CLI smoke) sends `role:"system"` entries in
|
|
153
|
-
* `messages` despite the published API having no system role.
|
|
154
|
-
*
|
|
155
|
-
* `
|
|
156
|
-
*
|
|
153
|
+
* `messages` despite the published API having no system role. They are emitted as
|
|
154
|
+
* chronological `role:"developer"` input items, which keeps the timeline intact and
|
|
155
|
+
* leaves `instructions` owned solely by the top-level Anthropic `system` field.
|
|
156
|
+
*
|
|
157
|
+
* The original mapping folded them into `instructions` because the native ChatGPT
|
|
158
|
+
* backend rejects `role:"system"` items in `input` ("System messages are not allowed",
|
|
159
|
+
* verified live). That constraint is real and still respected — but it only rules out
|
|
160
|
+
* `system`, not `developer`, which every Responses route accepts. Folding meant each
|
|
161
|
+
* mid-conversation reminder mutated the prompt head, invalidating the upstream KV
|
|
162
|
+
* prefix and rotating the Desktop `prompt_cache_key` fallback below on every turn
|
|
163
|
+
* (#4148).
|
|
157
164
|
*/
|
|
158
165
|
function systemMessageText(content: unknown): string {
|
|
159
166
|
if (typeof content === "string") return content;
|
|
@@ -333,7 +340,12 @@ function translateAnthropicRequest(raw: unknown, cc: OcxClaudeCodeConfig | undef
|
|
|
333
340
|
else if (msg.role === "assistant") assistantMessageToItems(msg.content, input, budget);
|
|
334
341
|
else if (msg.role === "system") {
|
|
335
342
|
const text = systemMessageText(msg.content);
|
|
336
|
-
|
|
343
|
+
// Keep it where the client put it. `developer` is first-class in the Responses
|
|
344
|
+
// schema and survives parseRequest as a chronological message, where `system`
|
|
345
|
+
// would be re-hoisted back onto the system prompt and defeat the point.
|
|
346
|
+
if (text.length > 0) {
|
|
347
|
+
input.push({ type: "message", role: "developer", content: [{ type: "input_text", text }] });
|
|
348
|
+
}
|
|
337
349
|
}
|
|
338
350
|
else throw new AnthropicRequestError(`unsupported message role: ${String(msg.role)}`);
|
|
339
351
|
}
|
package/src/cli/account-api.ts
CHANGED
|
@@ -18,10 +18,16 @@ export interface AccountRow {
|
|
|
18
18
|
id: string;
|
|
19
19
|
label?: string;
|
|
20
20
|
email?: string;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Subscription tier. `null` means this version looked and the provider did not report one;
|
|
23
|
+
* an absent key means the row's surface does not carry a tier at all (#3777).
|
|
24
|
+
*/
|
|
25
|
+
plan?: string | null;
|
|
22
26
|
masked?: string;
|
|
23
27
|
active: boolean;
|
|
24
28
|
needsReauth?: boolean;
|
|
29
|
+
/** Registered credential that is still excluded from routing until validation completes. */
|
|
30
|
+
validationPending?: boolean;
|
|
25
31
|
/** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
|
|
26
32
|
priority?: number;
|
|
27
33
|
quota?: CodexQuotaDto | null;
|
|
@@ -237,6 +243,7 @@ interface CodexAccountDto {
|
|
|
237
243
|
plan?: string;
|
|
238
244
|
isMain?: boolean;
|
|
239
245
|
needsReauth?: boolean;
|
|
246
|
+
health?: { reason?: string };
|
|
240
247
|
priority?: number;
|
|
241
248
|
quota?: CodexQuotaDto | null;
|
|
242
249
|
quotaRefresh?: unknown;
|
|
@@ -263,10 +270,12 @@ export async function fetchCodexRows(
|
|
|
263
270
|
baseUrl: string,
|
|
264
271
|
forceRefresh = false,
|
|
265
272
|
includeQuota = forceRefresh,
|
|
273
|
+
options: { refreshAction?: boolean } = {},
|
|
266
274
|
): Promise<FamilyRows> {
|
|
267
|
-
const
|
|
275
|
+
const refreshAction = options.refreshAction === true;
|
|
276
|
+
const accountsPath = `/api/codex-auth/accounts${refreshAction ? "/refresh" : forceRefresh ? "?refresh=1" : ""}`;
|
|
268
277
|
const [accountsRes, activeRes] = await Promise.all([
|
|
269
|
-
apiJson(deps, baseUrl, "GET", accountsPath),
|
|
278
|
+
apiJson(deps, baseUrl, refreshAction ? "POST" : "GET", accountsPath),
|
|
270
279
|
apiJson(deps, baseUrl, "GET", "/api/codex-auth/active"),
|
|
271
280
|
]);
|
|
272
281
|
if (accountsRes.status !== 0 && accountsRes.status !== 200) {
|
|
@@ -300,6 +309,7 @@ export async function fetchCodexRows(
|
|
|
300
309
|
plan: a.plan,
|
|
301
310
|
active: a.id === activeId,
|
|
302
311
|
needsReauth: a.needsReauth,
|
|
312
|
+
...(a.health?.reason === "validation_pending" ? { validationPending: true } : {}),
|
|
303
313
|
priority: typeof a.priority === "number" ? a.priority : 0,
|
|
304
314
|
paused: a.paused === true,
|
|
305
315
|
...(includeQuota ? {
|
|
@@ -316,6 +326,8 @@ interface OAuthAccountDto {
|
|
|
316
326
|
email?: string;
|
|
317
327
|
active?: boolean;
|
|
318
328
|
needsReauth?: boolean;
|
|
329
|
+
/** Always sent by the management route; explicitly `null` when the tier is unknown. */
|
|
330
|
+
plan?: string | null;
|
|
319
331
|
quota?: CodexQuotaDto | null;
|
|
320
332
|
quotaUnavailable?: boolean;
|
|
321
333
|
}
|
|
@@ -346,6 +358,9 @@ async function fetchOAuthRows(
|
|
|
346
358
|
email: a.email,
|
|
347
359
|
active: a.active ?? a.id === activeId,
|
|
348
360
|
needsReauth: a.needsReauth,
|
|
361
|
+
// Forward the server's answer verbatim, including `null`. Collapsing null to "absent" here
|
|
362
|
+
// would destroy the one distinction this field exists to make.
|
|
363
|
+
plan: a.plan ?? null,
|
|
349
364
|
...(a.quota !== undefined ? { quota: a.quota } : {}),
|
|
350
365
|
...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
|
|
351
366
|
}));
|
package/src/cli/account-auth.ts
CHANGED
|
@@ -154,7 +154,14 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
154
154
|
{}, deps,
|
|
155
155
|
);
|
|
156
156
|
if (state.status === "done") {
|
|
157
|
-
|
|
157
|
+
if (state.validationPending === true) {
|
|
158
|
+
printData({ ...state, recoveryCommand: "ocx gui", recoveryAction: "After quota recovers, click Refresh quotas in the dashboard Codex account pool." }, wantsJson, [
|
|
159
|
+
"Account registered; validation pending (routing disabled).",
|
|
160
|
+
"After quota recovers, open 'ocx gui' and click Refresh quotas to complete validation.",
|
|
161
|
+
]);
|
|
162
|
+
} else {
|
|
163
|
+
printData({ ...state, modelSelection: modelSelectionNextSteps("openai") }, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`, ...modelSelectionGuidance("openai")]);
|
|
164
|
+
}
|
|
158
165
|
if (!wantsJson) warnIfCodexCatalogRefreshPending(state);
|
|
159
166
|
return;
|
|
160
167
|
}
|