@bitkyc08/opencodex 2.11.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
|
@@ -538,6 +538,65 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknow
|
|
|
538
538
|
return changed ? { ...body, input: repaired } : body;
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Make unambiguous Responses tool pairs adjacent for upstream parsers that require it.
|
|
543
|
+
*
|
|
544
|
+
* [Decision Log]
|
|
545
|
+
* - 목적과 의도: Keep Codex hook-injected developer context without letting it make a strict upstream reject the matching tool result.
|
|
546
|
+
* - 기존 구현 및 제약 조건: The orphan repair verifies only pair presence; globally reordering valid history would change tolerant providers unnecessarily.
|
|
547
|
+
* - 검토한 주요 대안: Reorder every Responses request, drop the intervening message, or gate a lossless reorder behind provider capability metadata.
|
|
548
|
+
* - 선택한 방식: Reorder only unique call/result pairs for providers that explicitly require adjacency, preserving every intervening item immediately after the result.
|
|
549
|
+
* - 다른 대안 대신 이 방식을 선택한 이유: The provider gate limits semantic blast radius, while refusing ambiguous duplicate ids avoids guessing which result belongs to which call.
|
|
550
|
+
* - 장점, 단점 및 영향: DeepSeek receives the adjacency its parser requires; tolerant providers stay byte/order equivalent. Ambiguous duplicate ids still fail upstream rather than being silently rewritten.
|
|
551
|
+
*/
|
|
552
|
+
function normalizeResponsesToolResultAdjacency(body: unknown): unknown {
|
|
553
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
554
|
+
const input = body.input;
|
|
555
|
+
const calls = new Map<string, number[]>();
|
|
556
|
+
const outputs = new Map<string, number[]>();
|
|
557
|
+
|
|
558
|
+
const appendIndex = (map: Map<string, number[]>, key: string, index: number): void => {
|
|
559
|
+
const existing = map.get(key);
|
|
560
|
+
if (existing) existing.push(index);
|
|
561
|
+
else map.set(key, [index]);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
565
|
+
const item = input[index];
|
|
566
|
+
if (!isPlainObject(item) || typeof item.call_id !== "string" || item.call_id.length === 0) continue;
|
|
567
|
+
if (item.type === "function_call" || item.type === "local_shell_call") {
|
|
568
|
+
appendIndex(calls, `function:${item.call_id}`, index);
|
|
569
|
+
} else if (item.type === "custom_tool_call") {
|
|
570
|
+
appendIndex(calls, `custom:${item.call_id}`, index);
|
|
571
|
+
} else if (item.type === "function_call_output") {
|
|
572
|
+
appendIndex(outputs, `function:${item.call_id}`, index);
|
|
573
|
+
} else if (item.type === "custom_tool_call_output") {
|
|
574
|
+
appendIndex(outputs, `custom:${item.call_id}`, index);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const movedOutputIndices = new Set<number>();
|
|
579
|
+
const outputAfterCall = new Map<number, unknown>();
|
|
580
|
+
for (const [key, callIndices] of calls) {
|
|
581
|
+
const outputIndices = outputs.get(key);
|
|
582
|
+
if (callIndices.length !== 1 || outputIndices?.length !== 1) continue;
|
|
583
|
+
const callIndex = callIndices[0]!;
|
|
584
|
+
const outputIndex = outputIndices[0]!;
|
|
585
|
+
if (outputIndex === callIndex + 1) continue;
|
|
586
|
+
movedOutputIndices.add(outputIndex);
|
|
587
|
+
outputAfterCall.set(callIndex, input[outputIndex]);
|
|
588
|
+
}
|
|
589
|
+
if (movedOutputIndices.size === 0) return body;
|
|
590
|
+
|
|
591
|
+
const normalized: unknown[] = [];
|
|
592
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
593
|
+
if (movedOutputIndices.has(index)) continue;
|
|
594
|
+
normalized.push(input[index]);
|
|
595
|
+
if (outputAfterCall.has(index)) normalized.push(outputAfterCall.get(index));
|
|
596
|
+
}
|
|
597
|
+
return { ...body, input: normalized };
|
|
598
|
+
}
|
|
599
|
+
|
|
541
600
|
/**
|
|
542
601
|
* Remove `previous_response_id` before forwarding. Two triggers:
|
|
543
602
|
* - the proxy expanded the request into a full input replay (the id is now redundant), or
|
|
@@ -1158,6 +1217,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1158
1217
|
if (forward || stateless) {
|
|
1159
1218
|
outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
|
|
1160
1219
|
}
|
|
1220
|
+
if (provider.requiresAdjacentResponsesToolResults === true) {
|
|
1221
|
+
outBody = normalizeResponsesToolResultAdjacency(outBody);
|
|
1222
|
+
}
|
|
1161
1223
|
if (forward) {
|
|
1162
1224
|
outBody = stripUnsupportedForwardParams(outBody);
|
|
1163
1225
|
} else {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
isAllowedToolChoice,
|
|
3
2
|
namespacedToolName,
|
|
4
|
-
|
|
5
|
-
toolChoiceAliases,
|
|
3
|
+
toolChoiceToolPredicate,
|
|
6
4
|
type OcxRequestOptions,
|
|
7
5
|
type OcxTool,
|
|
8
6
|
type OcxProviderConfig,
|
|
@@ -18,13 +16,6 @@ function uniqueNames(names: readonly string[]): string[] {
|
|
|
18
16
|
return [...new Set(names.filter(name => name.trim().length > 0))];
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
function toolChoiceAllows(tool: Pick<OcxTool, "namespace" | "name">, toolChoice: OcxRequestOptions["toolChoice"] | undefined): boolean {
|
|
22
|
-
if (!toolChoice || toolChoice === "auto" || toolChoice === "required") return true;
|
|
23
|
-
if (toolChoice === "none") return false;
|
|
24
|
-
if (isAllowedToolChoice(toolChoice)) return toolAllowedByChoice(tool, new Set(toolChoice.allowedTools));
|
|
25
|
-
return toolChoiceAliases(tool).includes(toolChoice.name);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
19
|
function isOpenAIOrChatGPTHost(hostname: string): boolean {
|
|
29
20
|
return hostname === "openai.com"
|
|
30
21
|
|| hostname.endsWith(".openai.com")
|
|
@@ -65,7 +56,7 @@ export function buildNonOpenAIToolCatalogNudgeForTools(
|
|
|
65
56
|
toWireName: (tool: Pick<OcxTool, "namespace" | "name">) => string = tool => namespacedToolName(tool.namespace, tool.name),
|
|
66
57
|
): string | undefined {
|
|
67
58
|
const visibleNames = tools
|
|
68
|
-
?.filter(
|
|
59
|
+
?.filter(toolChoiceToolPredicate(toolChoice))
|
|
69
60
|
.map(toWireName);
|
|
70
61
|
return buildNonOpenAIToolCatalogNudgeFromNames(visibleNames);
|
|
71
62
|
}
|
package/src/claude/outbound.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - message_delta.usage is cumulative; message_start embeds a full message snapshot.
|
|
10
10
|
* - errors: {type:"error", error:{type,message}}; may arrive mid-stream after HTTP 200.
|
|
11
11
|
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
12
13
|
import { isTransientUpstreamStatus } from "../lib/upstream-retry";
|
|
13
14
|
import {
|
|
14
15
|
isTranslatorBudgetExceededError,
|
|
@@ -24,6 +25,27 @@ function isRec(v: unknown): v is Rec {
|
|
|
24
25
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function reasoningIdentityDigest(value: string): string {
|
|
29
|
+
return createHash("sha256").update(value).digest("base64url");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Fixed-size identity that preserves protocol boundaries without retaining upstream strings. */
|
|
33
|
+
function boundedReasoningIdentity(value: unknown): string {
|
|
34
|
+
if (typeof value === "number") {
|
|
35
|
+
if (Number.isSafeInteger(value) && value >= 0) return `n${value}`;
|
|
36
|
+
if (Number.isFinite(value)) return `d${value}`;
|
|
37
|
+
return Number.isNaN(value) ? "dnan" : value > 0 ? "dinf" : "d-inf";
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
return `s${reasoningIdentityDigest(value)}`;
|
|
41
|
+
}
|
|
42
|
+
if (value === null) return "z";
|
|
43
|
+
if (typeof value === "boolean") return value ? "b1" : "b0";
|
|
44
|
+
if (Array.isArray(value)) return `a${reasoningIdentityDigest(JSON.stringify(value) ?? "[]")}`;
|
|
45
|
+
// Preserve the prior String(record) category semantics without serializing untrusted trees.
|
|
46
|
+
return typeof value === "object" ? "o" : "u";
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
function uuid(): string {
|
|
28
50
|
return crypto.randomUUID().replace(/-/g, "");
|
|
29
51
|
}
|
|
@@ -188,7 +210,7 @@ interface OpenBlock {
|
|
|
188
210
|
argsBufBytes?: number;
|
|
189
211
|
webSearchArgsEmitted?: boolean;
|
|
190
212
|
callId?: string;
|
|
191
|
-
/** Last reasoning
|
|
213
|
+
/** Last fixed-size reasoning identity (item + summary/content index) seen by this block. */
|
|
192
214
|
reasoningPartKey?: string;
|
|
193
215
|
}
|
|
194
216
|
|
|
@@ -245,14 +267,13 @@ export function responsesSseToAnthropicSse(
|
|
|
245
267
|
emit("message_start", { type: "message_start", message: messageSnapshot(model) });
|
|
246
268
|
emit("ping", { type: "ping" });
|
|
247
269
|
};
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
270
|
+
// Once a semantic Anthropic message has started, keepalive pings protect remote
|
|
271
|
+
// deployments behind LB/NAT idle timeouts. Transport-only Responses prelude frames
|
|
272
|
+
// must not manufacture a message before a possible initial error.
|
|
251
273
|
if (pingIntervalMs > 0) {
|
|
252
274
|
pingTimer = setInterval(() => {
|
|
253
|
-
if (terminated) return;
|
|
275
|
+
if (terminated || !started) return;
|
|
254
276
|
try {
|
|
255
|
-
ensureStarted();
|
|
256
277
|
emit("ping", { type: "ping" });
|
|
257
278
|
} catch { /* controller torn down; the read loop is ending anyway */ }
|
|
258
279
|
}, pingIntervalMs);
|
|
@@ -328,20 +349,24 @@ export function responsesSseToAnthropicSse(
|
|
|
328
349
|
))));
|
|
329
350
|
return;
|
|
330
351
|
}
|
|
331
|
-
ensureStarted();
|
|
332
|
-
closeOpenBlock();
|
|
333
352
|
const type = upstreamDerived && isTransientUpstreamStatus(status) ? "overloaded_error" : undefined;
|
|
353
|
+
if (!started) {
|
|
354
|
+
// An initial upstream failure is an Anthropic error stream, not a partial message.
|
|
355
|
+
// Do not manufacture message_start/ping before the terminal error.
|
|
356
|
+
emit("error", anthropicErrorBody(status, message, type, code));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
closeOpenBlock();
|
|
334
360
|
emit("error", anthropicErrorBody(status, message, type, code));
|
|
335
361
|
};
|
|
336
362
|
|
|
337
363
|
const handleFrame = (eventName: string, data: Rec) => {
|
|
338
364
|
switch (eventName) {
|
|
339
365
|
case "response.created":
|
|
340
|
-
|
|
366
|
+
// Transport prelude only. Start Anthropic framing on semantic output or completion.
|
|
341
367
|
break;
|
|
342
368
|
case "response.heartbeat":
|
|
343
|
-
|
|
344
|
-
emit("ping", { type: "ping" });
|
|
369
|
+
if (started) emit("ping", { type: "ping" });
|
|
345
370
|
break;
|
|
346
371
|
case "response.output_text.delta": {
|
|
347
372
|
if (typeof data.delta !== "string" || data.delta.length === 0) break;
|
|
@@ -361,9 +386,12 @@ export function responsesSseToAnthropicSse(
|
|
|
361
386
|
// so multi-part summaries do not glue into one run-on paragraph. Frames
|
|
362
387
|
// without part indices produce a constant key and never get a separator.
|
|
363
388
|
const slot = eventName === "response.reasoning_summary_text.delta"
|
|
364
|
-
? `s${
|
|
365
|
-
: `c${
|
|
366
|
-
|
|
389
|
+
? `s${boundedReasoningIdentity(data.summary_index)}`
|
|
390
|
+
: `c${boundedReasoningIdentity(data.content_index)}`;
|
|
391
|
+
// Upstream string metadata can be arbitrarily large. Hash strings into fixed-size
|
|
392
|
+
// components while retaining item and part equality, rather than dropping item_id and
|
|
393
|
+
// accidentally joining distinct malformed reasoning items.
|
|
394
|
+
const partKey = `${boundedReasoningIdentity(data.item_id)}:${slot}`;
|
|
367
395
|
if (open!.reasoningPartKey !== undefined && open!.reasoningPartKey !== partKey) {
|
|
368
396
|
emit("content_block_delta", {
|
|
369
397
|
type: "content_block_delta", index: open!.index,
|
package/src/cli/claude.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { injectClaudeAgentDefs } from "../claude/agents-inject";
|
|
|
12
12
|
import { effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
|
|
13
13
|
import { refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
|
|
14
14
|
import { commandInvocation } from "../lib/win-exec";
|
|
15
|
+
import { isProxyAdmissionSecret } from "../server/auth-cors";
|
|
15
16
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
16
17
|
import type { OcxConfig } from "../types";
|
|
17
18
|
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
@@ -34,6 +35,29 @@ export type ClaudeEnvDeps = {
|
|
|
34
35
|
preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
function isClaudeLoopbackHostname(hostname: string): boolean {
|
|
39
|
+
const normalized = hostname.toLowerCase().replace(/\.$/, "");
|
|
40
|
+
return normalized === "localhost"
|
|
41
|
+
|| normalized === "127.0.0.1"
|
|
42
|
+
|| normalized === "::1"
|
|
43
|
+
|| normalized === "[::1]";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function targetsLocalClaudeProxy(value: string | undefined, port: number): boolean {
|
|
47
|
+
if (!value) return false;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(value);
|
|
50
|
+
const effectivePort = parsed.port === "" ? 80 : Number(parsed.port);
|
|
51
|
+
return parsed.protocol === "http:"
|
|
52
|
+
&& isClaudeLoopbackHostname(parsed.hostname)
|
|
53
|
+
&& effectivePort === port
|
|
54
|
+
&& parsed.username === ""
|
|
55
|
+
&& parsed.password === "";
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
/**
|
|
38
62
|
* Pure env assembly (unit-tested): never sets ANTHROPIC_API_KEY (setting both
|
|
39
63
|
* token vars triggers Claude Code's auth-conflict warning, 003 E1), and never
|
|
@@ -54,7 +78,7 @@ export function buildClaudeEnv(
|
|
|
54
78
|
// stale marker left in place would suppress the admission key and then be removed,
|
|
55
79
|
// leaving the child with no token at all (audit R2-1). It is opencodex state, never
|
|
56
80
|
// user auth, so dropping it unconditionally is safe.
|
|
57
|
-
if (env.ANTHROPIC_AUTH_TOKEN === PROXY_MARKER) delete env.ANTHROPIC_AUTH_TOKEN;
|
|
81
|
+
if (env.ANTHROPIC_AUTH_TOKEN?.trim() === PROXY_MARKER) delete env.ANTHROPIC_AUTH_TOKEN;
|
|
58
82
|
// Step 1b — drop Anthropic credentials AND destinations that Bun may have synthesized
|
|
59
83
|
// from a project `.env`/`.env.local`. The plain-Node launcher records genuine parent
|
|
60
84
|
// exports before Bun starts and pairs that context with an argv proof, so with a
|
|
@@ -95,10 +119,12 @@ export function buildClaudeEnv(
|
|
|
95
119
|
if (existingBaseUrl) {
|
|
96
120
|
try {
|
|
97
121
|
const parsed = new URL(existingBaseUrl);
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
122
|
+
const effectivePort = parsed.port === "" ? 80 : Number(parsed.port);
|
|
123
|
+
if (parsed.protocol === "http:"
|
|
124
|
+
&& isClaudeLoopbackHostname(parsed.hostname)
|
|
125
|
+
&& effectivePort !== port) {
|
|
100
126
|
const replacement = `http://127.0.0.1:${port}`;
|
|
101
|
-
console.error(`⚠ Replacing stale opencodex ANTHROPIC_BASE_URL ${
|
|
127
|
+
console.error(`⚠ Replacing stale opencodex ANTHROPIC_BASE_URL ${parsed.origin} with ${replacement}.`);
|
|
102
128
|
env.ANTHROPIC_BASE_URL = replacement;
|
|
103
129
|
}
|
|
104
130
|
} catch {
|
|
@@ -110,8 +136,26 @@ export function buildClaudeEnv(
|
|
|
110
136
|
// the user's Claude login. Only inject a token when the proxy actually requires an
|
|
111
137
|
// admission key; otherwise Claude Code keeps its own OAuth and sends it to us —
|
|
112
138
|
// native claude models then pass through verbatim (see server/claude-messages.ts).
|
|
113
|
-
|
|
114
|
-
|
|
139
|
+
const ownTokens = ownAdmissionTokens(config);
|
|
140
|
+
const targetsLocalProxy = targetsLocalClaudeProxy(env.ANTHROPIC_BASE_URL, port);
|
|
141
|
+
const inheritedApiKey = env.ANTHROPIC_API_KEY;
|
|
142
|
+
if (typeof inheritedApiKey === "string" && isProxyAdmissionSecret(inheritedApiKey, config)) {
|
|
143
|
+
delete env.ANTHROPIC_API_KEY;
|
|
144
|
+
}
|
|
145
|
+
const hasUserApiKey = Boolean(env.ANTHROPIC_API_KEY?.trim());
|
|
146
|
+
const inheritedAuthToken = env.ANTHROPIC_AUTH_TOKEN;
|
|
147
|
+
const inheritedTokenIsOurs = typeof inheritedAuthToken === "string"
|
|
148
|
+
&& isProxyAdmissionSecret(inheritedAuthToken, config);
|
|
149
|
+
// system-env may have injected the proxy's admission key into the parent. A
|
|
150
|
+
// proof-bound external BASE_URL is still user-owned, so never let our inherited
|
|
151
|
+
// key follow it. A user API key also wins on a local launch; remove only the token
|
|
152
|
+
// values recognized by the shared proxy-admission contract and preserve every
|
|
153
|
+
// other token.
|
|
154
|
+
if (inheritedTokenIsOurs && (!targetsLocalProxy || hasUserApiKey)) {
|
|
155
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
156
|
+
}
|
|
157
|
+
if (targetsLocalProxy && !hasUserApiKey && ownTokens.length > 0) {
|
|
158
|
+
setDefault("ANTHROPIC_AUTH_TOKEN", ownTokens[0]);
|
|
115
159
|
}
|
|
116
160
|
// Detection reads the SANITIZED launch env — the exact object spawned below — so the
|
|
117
161
|
// resolver and the spawned process cannot disagree. It deliberately does NOT read the
|
|
@@ -126,11 +170,19 @@ export function buildClaudeEnv(
|
|
|
126
170
|
...defaultAuthDetectDeps(env as NodeJS.ProcessEnv),
|
|
127
171
|
...(deps.authDetect ?? {}),
|
|
128
172
|
env: () => env as NodeJS.ProcessEnv,
|
|
129
|
-
ownTokens
|
|
173
|
+
ownTokens,
|
|
130
174
|
}));
|
|
131
|
-
if (!env.ANTHROPIC_AUTH_TOKEN && resolved.markerMode === "proxy") {
|
|
175
|
+
if (!env.ANTHROPIC_AUTH_TOKEN && !hasUserApiKey && targetsLocalProxy && resolved.markerMode === "proxy") {
|
|
132
176
|
env.ANTHROPIC_AUTH_TOKEN = PROXY_MARKER;
|
|
133
177
|
}
|
|
178
|
+
const finalAuthToken = env.ANTHROPIC_AUTH_TOKEN;
|
|
179
|
+
const hostOwnsAuthentication = targetsLocalProxy
|
|
180
|
+
&& !hasUserApiKey
|
|
181
|
+
&& typeof finalAuthToken === "string"
|
|
182
|
+
&& (
|
|
183
|
+
finalAuthToken.trim() === PROXY_MARKER
|
|
184
|
+
|| isProxyAdmissionSecret(finalAuthToken, config)
|
|
185
|
+
);
|
|
134
186
|
if (resolved.origin === "auto-unknown") {
|
|
135
187
|
console.error("⚠ Claude 인증을 확인하지 못했습니다 — 구독 방식으로 진행합니다. GUI에서 인증 모드를 직접 지정하면 이 판단을 덮어쓸 수 있습니다.");
|
|
136
188
|
}
|
|
@@ -152,7 +204,7 @@ export function buildClaudeEnv(
|
|
|
152
204
|
// Claude Code 2.1.206+ also treats this as a host-auth assertion. Injecting it
|
|
153
205
|
// without a host token makes a valid claude.ai subscription look logged out,
|
|
154
206
|
// so the guard is only safe when opencodex actually owns authentication.
|
|
155
|
-
if (
|
|
207
|
+
if (hostOwnsAuthentication) {
|
|
156
208
|
setDefault("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST", "1");
|
|
157
209
|
}
|
|
158
210
|
// Opt-in effort forcing (devlog 136 B6): opus-shaped aliases already carry
|
|
@@ -17,6 +17,8 @@ const DEFAULT_DEPS: CodexShimAutoRestoreCliDeps = {
|
|
|
17
17
|
|
|
18
18
|
export function skipsCodexShimAutoRestore(command: string | undefined, args: string[]): boolean {
|
|
19
19
|
if (command === "uninstall" || command === "remove") return true;
|
|
20
|
+
// `lab` is read-only inspection; it must not trigger shim side effects.
|
|
21
|
+
if (command === "lab") return true;
|
|
20
22
|
return command === "codex-shim" && ["install", "uninstall", "remove"].includes(args[1] ?? "");
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -60,9 +60,16 @@ export function codexShimReadinessWarnings(
|
|
|
60
60
|
|
|
61
61
|
export function collectCodexShimReadinessWarnings(): string[] {
|
|
62
62
|
const config = loadConfig();
|
|
63
|
+
let externalProvider: string | null = null;
|
|
64
|
+
try {
|
|
65
|
+
externalProvider = currentExternalCodexModelProvider();
|
|
66
|
+
} catch {
|
|
67
|
+
// Routing readiness is advisory; getCodexRoutingKind() already reports an
|
|
68
|
+
// unreadable config as unknown, so preserve that warning instead of failing.
|
|
69
|
+
}
|
|
63
70
|
return codexShimReadinessWarnings({
|
|
64
71
|
routingKind: getCodexRoutingKind(),
|
|
65
|
-
externalProvider
|
|
72
|
+
externalProvider,
|
|
66
73
|
processProxyEnvPresent: PROXY_ENV_KEYS.some(key => Boolean(process.env[key]?.trim())),
|
|
67
74
|
configuredProxyResolved: Boolean(resolveEnvValue(config.proxy)?.trim()),
|
|
68
75
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { clearCodexAccountPin } from "../codex/account-priority";
|
|
3
|
-
import { getConfigPath, readConfigDiagnostics, saveConfig, validateConfigCandidate } from "../config";
|
|
3
|
+
import { getConfigPath, readConfigDiagnostics, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate } from "../config";
|
|
4
4
|
import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../reasoning-effort";
|
|
5
5
|
import type { OcxConfig } from "../types";
|
|
6
6
|
import { normalizeVisionReasoningForModel } from "../vision/reasoning";
|
|
@@ -20,6 +20,9 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
|
20
20
|
|
|
21
21
|
function redact(value: unknown, key = ""): unknown {
|
|
22
22
|
if (SECRET_KEYS.test(key) && typeof value === "string") return value ? "********" : value;
|
|
23
|
+
// modelCosts rows are keyed by model id; a pasted API key in a key position
|
|
24
|
+
// must not be echoed back by config show/get (values are already redacted).
|
|
25
|
+
if (key === "modelCosts") return sanitizeModelCostsForDisplay(value);
|
|
23
26
|
if (Array.isArray(value)) return value.map(item => redact(item));
|
|
24
27
|
if (value && typeof value === "object") {
|
|
25
28
|
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([childKey, child]) => [childKey, redact(child, childKey)]));
|
package/src/cli/doctor.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { withNativeMainSharedClaim } from "../codex/native-main-claim";
|
|
|
23
23
|
import { probeNativeProfileRecoveryState, resolveNativeProfileContext } from "../codex/native-profile-store";
|
|
24
24
|
import { NativeProfileError } from "../codex/native-profile-types";
|
|
25
25
|
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
26
|
+
import { scanCodexAgentRolesWithTomlModelFallback } from "../codex/subagent-model-fallback";
|
|
26
27
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
27
28
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
28
29
|
import {
|
|
@@ -942,6 +943,15 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
942
943
|
}
|
|
943
944
|
}
|
|
944
945
|
|
|
946
|
+
console.log("\nCodex agent role files");
|
|
947
|
+
const tomlFallbackRoles = scanCodexAgentRolesWithTomlModelFallback(resolveCodexHomeDirImpl());
|
|
948
|
+
if (tomlFallbackRoles.length === 0) {
|
|
949
|
+
console.log(" ok no per-role model_fallback fields in $CODEX_HOME/agents/*.toml");
|
|
950
|
+
} else {
|
|
951
|
+
console.log(` [WARN] ${tomlFallbackRoles.length} agent role file${tomlFallbackRoles.length === 1 ? "" : "s"} contain${tomlFallbackRoles.length === 1 ? "s" : ""} \`model_fallback\`: ${tomlFallbackRoles.join(", ")}`);
|
|
952
|
+
console.log(" Codex >= 0.146 rejects that field as unknown and skips the whole role. Move the chains to opencodex config `subagentModelFallbackByModel` (keyed by primary model) and remove the field from the TOML files.");
|
|
953
|
+
}
|
|
954
|
+
|
|
945
955
|
const dual = collectWslDualInstall();
|
|
946
956
|
if (dual.wsl) {
|
|
947
957
|
console.log("\nWSL Codex installs");
|
package/src/cli/help.ts
CHANGED
|
@@ -264,6 +264,20 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
264
264
|
"Invalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.",
|
|
265
265
|
],
|
|
266
266
|
},
|
|
267
|
+
lab: {
|
|
268
|
+
usage: "ocx lab <status|verdicts|subjects|subject|observations|events|event|artifacts|artifact|catalog> [options] [--json]",
|
|
269
|
+
summary: "Read-only Compatibility Lab projection inspection (local SQLite; no daemon).",
|
|
270
|
+
details: [
|
|
271
|
+
"status Projection availability, schema versions, and row counts.",
|
|
272
|
+
"verdicts Paginated derived compatibility verdicts with filters.",
|
|
273
|
+
"subjects List subjects; subject <id> returns one typed subject.",
|
|
274
|
+
"observations Paginated observation rows from the projection.",
|
|
275
|
+
"events Event history; event <id> returns one safe typed event.",
|
|
276
|
+
"artifacts Artifact metadata only (no content download).",
|
|
277
|
+
"catalog Packaged protocol/live scenario catalog metadata.",
|
|
278
|
+
"Reads never rebuild the projection, trigger probes, or require the proxy.",
|
|
279
|
+
],
|
|
280
|
+
},
|
|
267
281
|
};
|
|
268
282
|
|
|
269
283
|
function packageVersion(): string {
|
|
@@ -318,6 +332,7 @@ Usage:
|
|
|
318
332
|
ocx grok <sub> Grok Build model selection and apply
|
|
319
333
|
ocx system <sub> Runtime settings, startup, sync, and updates
|
|
320
334
|
ocx config <sub> Validated configuration show/get/set/import/export
|
|
335
|
+
ocx lab <sub> Read-only Compatibility Lab projection inspection
|
|
321
336
|
ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
|
|
322
337
|
ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
|
|
323
338
|
ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)
|
package/src/cli/index.ts
CHANGED
|
@@ -219,6 +219,22 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
async function findProxyOwnerBeforeJournalRecovery(
|
|
223
|
+
options: { probeConfiguredPort?: boolean } = {},
|
|
224
|
+
): Promise<{ live: LiveProxy | null; pidSnapshot: number | null }> {
|
|
225
|
+
const pidSnapshot = readPidFileValue();
|
|
226
|
+
const hasRuntimeOwner = readRuntimePort() !== null;
|
|
227
|
+
const shouldProbe = pidSnapshot !== null || hasRuntimeOwner || options.probeConfiguredPort === true;
|
|
228
|
+
const live = shouldProbe ? await findLiveProxy() : null;
|
|
229
|
+
if (live) return { live, pidSnapshot };
|
|
230
|
+
|
|
231
|
+
// The probe established that the snapshotted owner is stale. Compare before
|
|
232
|
+
// deleting so a concurrent start that rewrote the PID file keeps its state.
|
|
233
|
+
removePidIfValueIs(pidSnapshot);
|
|
234
|
+
if (!currentExternalCodexModelProvider()) reconcileJournal();
|
|
235
|
+
return { live: null, pidSnapshot };
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
async function handleStart(options: { block?: boolean } = {}) {
|
|
223
239
|
// Native (WinSW) service mode has no batch wrapper to read the service token file
|
|
224
240
|
// into the environment, so the app loads it here before the server binds. The server
|
|
@@ -226,15 +242,10 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
226
242
|
const serviceToken = loadServiceTokenFromFile(process.env);
|
|
227
243
|
if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
|
|
228
244
|
const requestedPort = parsePortOption();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (live) {
|
|
234
|
-
console.error(`⚠️ Proxy already running (PID ${live.pid ?? existingPid}, port ${live.port}). Use 'ocx stop' first.`);
|
|
235
|
-
process.exit(1);
|
|
236
|
-
}
|
|
237
|
-
removePid(existingPid);
|
|
245
|
+
const owner = await findProxyOwnerBeforeJournalRecovery();
|
|
246
|
+
if (owner.live) {
|
|
247
|
+
console.error(`⚠️ Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}). Use 'ocx stop' first.`);
|
|
248
|
+
process.exit(1);
|
|
238
249
|
}
|
|
239
250
|
|
|
240
251
|
// Interactive-only update prompt. Must run BEFORE we bind a port / write a
|
|
@@ -442,13 +453,13 @@ function detachedStartEnvironment(): NodeJS.ProcessEnv {
|
|
|
442
453
|
}
|
|
443
454
|
|
|
444
455
|
async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Promise<boolean> {
|
|
445
|
-
|
|
456
|
+
const owner = await findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true });
|
|
446
457
|
const config = loadConfig();
|
|
447
458
|
if (!codexAutoStartEnabled(config)) {
|
|
448
459
|
console.log("Codex autostart is disabled.");
|
|
449
460
|
return false;
|
|
450
461
|
}
|
|
451
|
-
const live =
|
|
462
|
+
const live = owner.live;
|
|
452
463
|
if (live) {
|
|
453
464
|
if (options.existingIsSuccess === false) {
|
|
454
465
|
console.error("Proxy appeared while restart was confirming absence; no start was attempted.");
|
|
@@ -1356,6 +1367,11 @@ switch (command) {
|
|
|
1356
1367
|
process.exitCode = await handleConfigCommand(args.slice(1));
|
|
1357
1368
|
break;
|
|
1358
1369
|
}
|
|
1370
|
+
case "lab": {
|
|
1371
|
+
const { handleLabCommand } = await import("./lab");
|
|
1372
|
+
process.exitCode = await handleLabCommand(args.slice(1));
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1359
1375
|
case "claude": {
|
|
1360
1376
|
const { cmdClaude } = await import("./claude");
|
|
1361
1377
|
// "ocx claude desktop" → write Desktop 3P config
|