@bitkyc08/opencodex 2.19.0 → 2.21.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/gui/dist/assets/index-BOFeam5a.js +76 -0
- package/gui/dist/assets/index-Xq49CY8F.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +392 -40
- package/src/adapters/registry.ts +144 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/chat/inbound.ts +13 -7
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +3 -2
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +241 -85
- package/src/images/loop.ts +26 -6
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +14 -4
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +50 -6
- package/src/server/chat-completions.ts +74 -36
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +257 -46
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/server/system-env.ts +1 -1
- package/src/types.ts +84 -5
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/src/web-search/loop.ts +21 -5
- package/gui/dist/assets/index-CQ7bIKee.css +0 -1
- package/gui/dist/assets/index-D_JUZLEC.js +0 -76
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Codex multi-agent v2 stamps a Responses-only `encrypted: true` marker on
|
|
2
|
+
// collaboration tool schemas (openai/codex 5f4d06ef; issue #85). It is an
|
|
3
|
+
// annotation for the ChatGPT backend only, so translated provider schemas must
|
|
4
|
+
// drop it without removing properties or definitions literally named `encrypted`.
|
|
5
|
+
const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set([
|
|
6
|
+
"properties",
|
|
7
|
+
"patternProperties",
|
|
8
|
+
"$defs",
|
|
9
|
+
"definitions",
|
|
10
|
+
"dependencies",
|
|
11
|
+
"dependentSchemas",
|
|
12
|
+
"dependentRequired",
|
|
13
|
+
]);
|
|
14
|
+
const ENCRYPTED_MARKER_LITERAL_VALUE_KEYS = new Set(["const", "default", "enum", "examples"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The schema is caller-supplied, so its nesting depth is attacker-influenced. Native recursion
|
|
18
|
+
* would turn a deep schema into a stack overflow that takes down the request path, so this walks
|
|
19
|
+
* an explicit stack instead: depth costs heap, which is bounded and recoverable.
|
|
20
|
+
*/
|
|
21
|
+
export function stripResponsesOnlyEncryptedMarker(node: unknown, inNameBag = false): unknown {
|
|
22
|
+
type Assign = (value: unknown) => void;
|
|
23
|
+
interface Frame { node: unknown; inNameBag: boolean; assign: Assign }
|
|
24
|
+
|
|
25
|
+
let result: unknown;
|
|
26
|
+
const stack: Frame[] = [{ node, inNameBag, assign: value => { result = value; } }];
|
|
27
|
+
|
|
28
|
+
while (stack.length > 0) {
|
|
29
|
+
const frame = stack.pop()!;
|
|
30
|
+
const current = frame.node;
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(current)) {
|
|
33
|
+
const out: unknown[] = new Array(current.length);
|
|
34
|
+
frame.assign(out);
|
|
35
|
+
// Array items are schemas in their own right, never a name bag.
|
|
36
|
+
for (let i = current.length - 1; i >= 0; i--) {
|
|
37
|
+
stack.push({ node: current[i], inNameBag: false, assign: value => { out[i] = value; } });
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!current || typeof current !== "object") {
|
|
42
|
+
frame.assign(current);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A schema name may be `__proto__`; a null-prototype record keeps it as data.
|
|
47
|
+
const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
48
|
+
frame.assign(out);
|
|
49
|
+
|
|
50
|
+
for (const [key, value] of Object.entries(current as Record<string, unknown>)) {
|
|
51
|
+
if (frame.inNameBag) {
|
|
52
|
+
// Inside a name bag every key is a caller-chosen name, so `encrypted` here is data.
|
|
53
|
+
stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
|
|
54
|
+
} else if (key !== "encrypted") {
|
|
55
|
+
if (ENCRYPTED_MARKER_LITERAL_VALUE_KEYS.has(key)) {
|
|
56
|
+
// Literal payloads are values, not schemas: an `encrypted` key inside them is data.
|
|
57
|
+
out[key] = value;
|
|
58
|
+
} else {
|
|
59
|
+
const childInNameBag = ENCRYPTED_MARKER_NAME_BAG_KEYS.has(key);
|
|
60
|
+
stack.push({ node: value, inNameBag: childInNameBag, assign: v => { out[key] = v; } });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
package/src/bridge.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AdapterEvent,
|
|
3
3
|
OcxMessagePhase,
|
|
4
4
|
OcxProviderContinuationState,
|
|
5
|
+
OcxProviderOpaqueToolCallMetadata,
|
|
5
6
|
OcxReasoningReplayScopeRef,
|
|
6
7
|
OcxUsage,
|
|
7
8
|
} from "./types";
|
|
@@ -10,6 +11,7 @@ import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCy
|
|
|
10
11
|
import { encodeCompactionSummary } from "./responses/compaction";
|
|
11
12
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
12
13
|
import { rememberReasoningForCall } from "./responses/reasoning-replay-cache";
|
|
14
|
+
import { responsesExtraContentFromProviderMetadata } from "./responses/provider-opaque-metadata";
|
|
13
15
|
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
14
16
|
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
15
17
|
import {
|
|
@@ -496,7 +498,7 @@ export function bridgeToResponsesSSE(
|
|
|
496
498
|
// synthetic compaction item's payload on done.
|
|
497
499
|
let compactionText = "";
|
|
498
500
|
let compactionTextBytes = 0;
|
|
499
|
-
let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string } | null = null;
|
|
501
|
+
let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
|
|
500
502
|
// Open native web-search cell (between begin and end). Holds the output index allocated on
|
|
501
503
|
// begin so the matching done reuses it; closed as `failed` if the stream terminates early.
|
|
502
504
|
let currentWebSearch: { itemId: string; eventId: string; outputIndex: number } | null = null;
|
|
@@ -621,6 +623,9 @@ export function bridgeToResponsesSSE(
|
|
|
621
623
|
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
622
624
|
arguments: argsStr, status: "completed",
|
|
623
625
|
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
|
|
626
|
+
// Provider-opaque metadata (issue #1735) rides the item so a client that replays
|
|
627
|
+
// this history can hand the signature back on the part it belongs to.
|
|
628
|
+
...(responsesExtraContentFromProviderMetadata(currentToolCall.providerMetadata) ?? {}),
|
|
624
629
|
};
|
|
625
630
|
emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
|
|
626
631
|
retainFinishedItem(item as OutputItem);
|
|
@@ -655,6 +660,10 @@ export function bridgeToResponsesSSE(
|
|
|
655
660
|
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
656
661
|
arguments: argsStr, status: "incomplete",
|
|
657
662
|
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
|
|
663
|
+
// An incomplete call can still be persisted and replayed (max_output_tokens), so it
|
|
664
|
+
// carries the same metadata as the completed item — otherwise SSE and buffered JSON
|
|
665
|
+
// would disagree about whether the signature survives.
|
|
666
|
+
...(responsesExtraContentFromProviderMetadata(currentToolCall.providerMetadata) ?? {}),
|
|
658
667
|
};
|
|
659
668
|
emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
|
|
660
669
|
retainFinishedItem(item as OutputItem);
|
|
@@ -1013,7 +1022,7 @@ export function bridgeToResponsesSSE(
|
|
|
1013
1022
|
? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, input: "", status: "in_progress" }
|
|
1014
1023
|
: { type: "function_call", id: itemId, call_id: event.id, name: realName, arguments: "", status: "in_progress", ...(ns ? { namespace: ns } : {}) };
|
|
1015
1024
|
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
1016
|
-
currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch };
|
|
1025
|
+
currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch, providerMetadata: event.providerMetadata };
|
|
1017
1026
|
budget?.openCall(event.id);
|
|
1018
1027
|
break;
|
|
1019
1028
|
}
|
|
@@ -1476,6 +1485,7 @@ function buildResponseJSONWithBudget(
|
|
|
1476
1485
|
let currentToolCallId = "";
|
|
1477
1486
|
let currentToolCallName = "";
|
|
1478
1487
|
let currentToolCallArgs = "";
|
|
1488
|
+
let currentToolCallProviderMetadata: OcxProviderOpaqueToolCallMetadata | undefined;
|
|
1479
1489
|
let currentToolCallArgsBytes = 0;
|
|
1480
1490
|
// Web-search citations awaiting the next assistant message (attached as url_citation annotations).
|
|
1481
1491
|
let pendingWebSources: { url: string; title?: string }[] = [];
|
|
@@ -1584,11 +1594,13 @@ function buildResponseJSONWithBudget(
|
|
|
1584
1594
|
call_id: currentToolCallId, name: realName,
|
|
1585
1595
|
arguments: coercedArgs || "{}", status,
|
|
1586
1596
|
...(ns ? { namespace: ns } : {}),
|
|
1597
|
+
...(responsesExtraContentFromProviderMetadata(currentToolCallProviderMetadata) ?? {}),
|
|
1587
1598
|
});
|
|
1588
1599
|
}
|
|
1589
1600
|
budget?.closeCall(currentToolCallId);
|
|
1590
1601
|
currentToolCallId = "";
|
|
1591
1602
|
currentToolCallName = "";
|
|
1603
|
+
currentToolCallProviderMetadata = undefined;
|
|
1592
1604
|
currentToolCallArgs = "";
|
|
1593
1605
|
currentToolCallArgsBytes = 0;
|
|
1594
1606
|
};
|
|
@@ -1703,6 +1715,7 @@ function buildResponseJSONWithBudget(
|
|
|
1703
1715
|
currentToolCallName = e.name;
|
|
1704
1716
|
currentToolCallArgs = "";
|
|
1705
1717
|
currentToolCallArgsBytes = 0;
|
|
1718
|
+
currentToolCallProviderMetadata = e.providerMetadata;
|
|
1706
1719
|
break;
|
|
1707
1720
|
case "tool_call_delta":
|
|
1708
1721
|
{
|
package/src/chat/inbound.ts
CHANGED
|
@@ -8,11 +8,23 @@
|
|
|
8
8
|
export class ChatCompletionsRequestError extends Error {}
|
|
9
9
|
|
|
10
10
|
type Rec = Record<string, unknown>;
|
|
11
|
+
type ChatCompletionsRoutingBody = Rec & { model: string; messages: unknown[] };
|
|
11
12
|
|
|
12
13
|
function isRec(v: unknown): v is Rec {
|
|
13
14
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/** Validate only the fields needed before Chat routing, without projecting the body. */
|
|
18
|
+
export function assertChatCompletionsRoutingBody(raw: unknown): asserts raw is ChatCompletionsRoutingBody {
|
|
19
|
+
if (!isRec(raw)) throw new ChatCompletionsRequestError("request body must be a JSON object");
|
|
20
|
+
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
21
|
+
throw new ChatCompletionsRequestError("model is required");
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(raw.messages) || raw.messages.length === 0) {
|
|
24
|
+
throw new ChatCompletionsRequestError("messages must be a non-empty array");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
17
29
|
const OUTPUT_CONFIG_SUMMARIES = new Set(["auto", "concise", "detailed", "none"]);
|
|
18
30
|
|
|
@@ -227,13 +239,7 @@ function resolveReasoningSummary(raw: Rec): string | undefined {
|
|
|
227
239
|
* Throws ChatCompletionsRequestError (-> 400) on malformed input.
|
|
228
240
|
*/
|
|
229
241
|
export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
230
|
-
|
|
231
|
-
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
232
|
-
throw new ChatCompletionsRequestError("model is required");
|
|
233
|
-
}
|
|
234
|
-
if (!Array.isArray(raw.messages) || raw.messages.length === 0) {
|
|
235
|
-
throw new ChatCompletionsRequestError("messages must be a non-empty array");
|
|
236
|
-
}
|
|
242
|
+
assertChatCompletionsRoutingBody(raw);
|
|
237
243
|
|
|
238
244
|
const systemParts: string[] = [];
|
|
239
245
|
const input: Rec[] = [];
|
|
@@ -13,12 +13,21 @@
|
|
|
13
13
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
17
|
+
import type { OcxConfig } from "../types";
|
|
16
18
|
|
|
17
19
|
export interface GatewayModelRow {
|
|
18
20
|
id: string;
|
|
19
21
|
display_name?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
export interface GatewayModelCacheRefreshOptions {
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
configDir?: string;
|
|
27
|
+
admissionConfig?: Pick<OcxConfig, "apiKeys">;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
/** Claude Code config dir (CLAUDE_CONFIG_DIR override honored, like the CLI). */
|
|
23
32
|
export function claudeConfigDir(): string {
|
|
24
33
|
const custom = process.env.CLAUDE_CONFIG_DIR;
|
|
@@ -45,14 +54,42 @@ export function writeGatewayModelCache(baseUrl: string, models: readonly Gateway
|
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Hardened service-token file, the same precedence `ocx opencode` uses. A service
|
|
59
|
+
* install writes the admission token to disk rather than the interactive environment,
|
|
60
|
+
* so an interactive `ocx claude` with neither env token nor configured key would
|
|
61
|
+
* otherwise still get a 401 and keep a stale picker list.
|
|
62
|
+
*/
|
|
63
|
+
function serviceFileToken(env: NodeJS.ProcessEnv): string | null {
|
|
64
|
+
const lookup = env.OCX_API_TOKEN_FILE?.trim()
|
|
65
|
+
? env
|
|
66
|
+
: { ...env, OCX_API_TOKEN_FILE: serviceApiTokenFilePath() };
|
|
67
|
+
return loadServiceTokenFromFile(lookup as Record<string, string | undefined>);
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
/** Fetch the anthropic-flavor /v1/models from the local proxy and write the cache. */
|
|
49
|
-
export async function refreshGatewayModelCacheFromProxy(
|
|
71
|
+
export async function refreshGatewayModelCacheFromProxy(
|
|
72
|
+
port: number,
|
|
73
|
+
options: GatewayModelCacheRefreshOptions = {},
|
|
74
|
+
): Promise<string | null> {
|
|
50
75
|
try {
|
|
76
|
+
const headers = new Headers({ "anthropic-version": "2023-06-01" });
|
|
77
|
+
// A wildcard/non-loopback listener requires data-plane admission even for a
|
|
78
|
+
// request sent to its local 127.0.0.1 address. Reuse the same dedicated
|
|
79
|
+
// credential domain as /v1/models admission; never place it in Authorization,
|
|
80
|
+
// which can belong to an upstream provider on other data-plane surfaces.
|
|
81
|
+
const envToken = (options.env ?? process.env).OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
82
|
+
const configuredToken = options.admissionConfig?.apiKeys
|
|
83
|
+
?.find(entry => entry.key.trim().length > 0)
|
|
84
|
+
?.key.trim();
|
|
85
|
+
const admissionToken = envToken || serviceFileToken(options.env ?? process.env) || configuredToken;
|
|
86
|
+
if (admissionToken) headers.set("x-opencodex-api-key", admissionToken);
|
|
87
|
+
|
|
51
88
|
// ?ids=cli pins the readable claude-ocx id family deterministically (audit 051
|
|
52
89
|
// #5): the cache prewrite must not depend on UA sniffing.
|
|
53
90
|
const res = await fetch(`http://127.0.0.1:${port}/v1/models?limit=1000&ids=cli`, {
|
|
54
|
-
headers
|
|
55
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
91
|
+
headers,
|
|
92
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 3_000),
|
|
56
93
|
});
|
|
57
94
|
if (!res.ok) return null;
|
|
58
95
|
const body = await res.json() as { data?: unknown };
|
|
@@ -63,7 +100,7 @@ export async function refreshGatewayModelCacheFromProxy(port: number, timeoutMs
|
|
|
63
100
|
id: m.id as string,
|
|
64
101
|
display_name: typeof m.display_name === "string" ? m.display_name : undefined,
|
|
65
102
|
}));
|
|
66
|
-
return writeGatewayModelCache(`http://127.0.0.1:${port}`, models, configDir);
|
|
103
|
+
return writeGatewayModelCache(`http://127.0.0.1:${port}`, models, options.configDir);
|
|
67
104
|
} catch {
|
|
68
105
|
return null;
|
|
69
106
|
}
|
package/src/cli/claude.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { configuredAdminToken } from "../lib/admin-secrets";
|
|
|
19
19
|
import { PROXY_MARKER, ownAdmissionTokens, defaultAuthDetectDeps, detectClaudeAuth, type AuthDetectDeps } from "../claude/auth-detect";
|
|
20
20
|
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
21
21
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
22
|
+
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
22
23
|
import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "./launcher-context";
|
|
23
24
|
|
|
24
25
|
export interface ClaudeLaunchEnv {
|
|
@@ -269,7 +270,7 @@ async function ensureProxyForClaude(): Promise<number | null> {
|
|
|
269
270
|
if (live) return live.port;
|
|
270
271
|
const cfgPort = loadConfig().port;
|
|
271
272
|
const pinPort = typeof cfgPort === "number" && cfgPort > 0 ? cfgPort : 10100;
|
|
272
|
-
const child = spawn(process.execPath, [
|
|
273
|
+
const child = spawn(process.execPath, selfLaunchArgv(["start", "--port", String(pinPort)]), {
|
|
273
274
|
detached: true,
|
|
274
275
|
stdio: "ignore",
|
|
275
276
|
windowsHide: true,
|
|
@@ -316,7 +317,7 @@ export async function cmdClaude(args: string[]): Promise<number> {
|
|
|
316
317
|
// Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
|
|
317
318
|
// never refreshes it, so the picker would keep showing yesterday's aliases.
|
|
318
319
|
try {
|
|
319
|
-
const cachePath = await refreshGatewayModelCacheFromProxy(port);
|
|
320
|
+
const cachePath = await refreshGatewayModelCacheFromProxy(port, { admissionConfig: config });
|
|
320
321
|
if (cachePath === null) {
|
|
321
322
|
console.error("⚠ Gateway model cache could not be refreshed; the model picker may be stale.");
|
|
322
323
|
}
|
package/src/cli/combo.ts
CHANGED
|
@@ -91,6 +91,9 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
91
91
|
if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
|
|
92
92
|
if (nativeAlias) combo.nativeAlias = true;
|
|
93
93
|
if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
|
|
94
|
+
const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
|
|
95
|
+
const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id));
|
|
96
|
+
if (existing?.imageInput === "disabled") combo.imageInput = "disabled";
|
|
94
97
|
const result = await runtimeRequest("/api/combos", {
|
|
95
98
|
method: "PUT",
|
|
96
99
|
body: JSON.stringify({ id, combo, ...(renameFrom ? { renameFrom } : {}) }),
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -488,6 +488,14 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
488
488
|
const { cmdOpencode } = await import("./opencode");
|
|
489
489
|
return await cmdOpencode(deps.args.slice(1));
|
|
490
490
|
},
|
|
491
|
+
mcode: async deps => {
|
|
492
|
+
const { cmdMcode } = await import("./minimax");
|
|
493
|
+
return await cmdMcode(deps.args.slice(1));
|
|
494
|
+
},
|
|
495
|
+
mmx: async deps => {
|
|
496
|
+
const { cmdMmx } = await import("./minimax");
|
|
497
|
+
return await cmdMmx(deps.args.slice(1));
|
|
498
|
+
},
|
|
491
499
|
help: async () => {
|
|
492
500
|
printUsage();
|
|
493
501
|
return 0;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ocx export --client <id>` — print a client config for the live proxy.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* are YAML
|
|
4
|
+
* Eight clients, four formats: OpenCode and Pi are JSON; OMP, Hermes, Gajae and
|
|
5
|
+
* MiniMax Code are YAML; OpenClaw is JSON5; Kimi is TOML.
|
|
6
6
|
*
|
|
7
7
|
* Two consumers, one payload (devlog 260731_client_config_export/020):
|
|
8
8
|
*
|
package/src/cli/help.ts
CHANGED
|
@@ -67,6 +67,8 @@ Usage:
|
|
|
67
67
|
ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
|
|
68
68
|
ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
|
|
69
69
|
ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)
|
|
70
|
+
ocx mcode [args...] Launch MiniMax Code through its managed provider
|
|
71
|
+
ocx mmx text <sub> [args] Launch MiniMax CLI text through the proxy
|
|
70
72
|
ocx help [command] Show help
|
|
71
73
|
ocx --version | -v Print version
|
|
72
74
|
|
package/src/cli/index.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { setIntegrationEnabled, shouldSyncCodexOnStart, shouldSyncGrokOnStart, s
|
|
|
60
60
|
|
|
61
61
|
import { removeOwnedConfigState } from "../lib/config-ownership";
|
|
62
62
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
63
|
+
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
63
64
|
import { initializeNodeLauncherContext } from "./launcher-context";
|
|
64
65
|
import { createLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
65
66
|
import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
|
|
@@ -121,11 +122,11 @@ function grokSyncFailureMessage(err: unknown): string {
|
|
|
121
122
|
|
|
122
123
|
/** Argv for detached `start`, optionally hard-pinning the listen port. */
|
|
123
124
|
function startArgv(port?: number): string[] {
|
|
124
|
-
const args = [
|
|
125
|
+
const args = ["start"];
|
|
125
126
|
if (typeof port === "number" && Number.isFinite(port) && port > 0 && port <= 65535) {
|
|
126
127
|
args.push("--port", String(Math.trunc(port)));
|
|
127
128
|
}
|
|
128
|
-
return args;
|
|
129
|
+
return selfLaunchArgv(args);
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
async function chooseListenPort(requestedPort?: number): Promise<number> {
|
package/src/cli/lab.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
queryPassiveProductionSignals,
|
|
37
37
|
type PassiveProductionQueryResultV1,
|
|
38
38
|
} from "../lab/query";
|
|
39
|
+
import { isLabRouteSubjectId } from "../usage/log";
|
|
39
40
|
import {
|
|
40
41
|
CliUsageError,
|
|
41
42
|
RuntimeApiError,
|
|
@@ -63,6 +64,14 @@ import { planManualLabRun } from "../lab/automation/planner";
|
|
|
63
64
|
import { listLabAutomationRuns } from "../lab/automation/runs-query";
|
|
64
65
|
import { LabAutomationError, type LabAutomationLayer } from "../lab/automation/types";
|
|
65
66
|
import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";
|
|
67
|
+
import {
|
|
68
|
+
exportLocalPublicEvidence,
|
|
69
|
+
importCommunityEvidenceFile,
|
|
70
|
+
listCommunityEvidenceContext,
|
|
71
|
+
previewLocalPublicEvidence,
|
|
72
|
+
verifyPublicEvidenceFile,
|
|
73
|
+
type PublicVerificationSummaryV1,
|
|
74
|
+
} from "../lab/public";
|
|
66
75
|
|
|
67
76
|
const USAGE = `Usage:
|
|
68
77
|
ocx lab status [--json]
|
|
@@ -76,6 +85,11 @@ const USAGE = `Usage:
|
|
|
76
85
|
ocx lab artifacts [--status <s>] [--artifact-class <c>] [--limit <n>] [--cursor <c>] [--json]
|
|
77
86
|
ocx lab artifact <digest> [--json]
|
|
78
87
|
ocx lab catalog [--layer <layer>] [--suite <id>] [--json]
|
|
88
|
+
ocx lab public preview --event <eventId> [--event <eventId> ...] [--json]
|
|
89
|
+
ocx lab public export --event <eventId> [--event <eventId> ...] [--json]
|
|
90
|
+
ocx lab public verify --file <bundle.json> [--json]
|
|
91
|
+
ocx lab public import --file <bundle.json> [--json]
|
|
92
|
+
ocx lab public community [--json]
|
|
79
93
|
ocx lab automation status [--json]
|
|
80
94
|
ocx lab automation enable [--protocol] [--live] [--json]
|
|
81
95
|
ocx lab automation disable [--json]
|
|
@@ -223,6 +237,119 @@ function runListLines(page: ReturnType<typeof listLabAutomationRuns>): string[]
|
|
|
223
237
|
return lines.length > 0 ? lines : ["No automation runs"];
|
|
224
238
|
}
|
|
225
239
|
|
|
240
|
+
function takeRepeatedOptions(args: string[], flag: string): string[] {
|
|
241
|
+
const values: string[] = [];
|
|
242
|
+
while (true) {
|
|
243
|
+
const index = args.indexOf(flag);
|
|
244
|
+
if (index < 0) break;
|
|
245
|
+
const value = args[index + 1];
|
|
246
|
+
if (!value || value.startsWith("--")) {
|
|
247
|
+
throw new CliUsageError(`${flag} requires a value`, USAGE);
|
|
248
|
+
}
|
|
249
|
+
values.push(value);
|
|
250
|
+
args.splice(index, 2);
|
|
251
|
+
}
|
|
252
|
+
return values;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function publicPreviewLines(result: ReturnType<typeof previewLocalPublicEvidence>): string[] {
|
|
256
|
+
return [
|
|
257
|
+
`Public evidence preview: ${result.bundle.records.length} exportable record(s)`,
|
|
258
|
+
`Excluded: ${result.excluded.length}`,
|
|
259
|
+
"Unsigned local preview; no publisher key or remote publish is created.",
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function publicExportLines(result: ReturnType<typeof exportLocalPublicEvidence>): string[] {
|
|
264
|
+
return [
|
|
265
|
+
"Public evidence exported locally",
|
|
266
|
+
`Bundle: ${result.bundle.bundleId}`,
|
|
267
|
+
`Publisher: ${result.bundle.publisher.keyId}`,
|
|
268
|
+
`Path: ${result.stored.path}`,
|
|
269
|
+
`Excluded: ${result.excluded.length}`,
|
|
270
|
+
"No remote publish occurred.",
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function publicVerificationLines(result: PublicVerificationSummaryV1): string[] {
|
|
275
|
+
if (result.status !== "cryptographically_valid") {
|
|
276
|
+
return [
|
|
277
|
+
`Public evidence verification: ${result.status}`,
|
|
278
|
+
"Not locally verified.",
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
return [
|
|
282
|
+
"Public evidence verification: cryptographically valid",
|
|
283
|
+
`Bundle: ${result.bundleId}`,
|
|
284
|
+
`Publisher: ${result.publisherKeyId}`,
|
|
285
|
+
"Not locally verified. Signature validity proves integrity/continuity only.",
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function handlePublicLabCommand(
|
|
290
|
+
argv: string[],
|
|
291
|
+
wantsJson: boolean,
|
|
292
|
+
configDir: string,
|
|
293
|
+
): void {
|
|
294
|
+
const [action, ...restInput] = argv;
|
|
295
|
+
const rest = [...restInput];
|
|
296
|
+
switch (action) {
|
|
297
|
+
case "preview": {
|
|
298
|
+
const eventIds = takeRepeatedOptions(rest, "--event");
|
|
299
|
+
rejectArgs(rest, USAGE);
|
|
300
|
+
const result = previewLocalPublicEvidence({ eventIds }, configDir);
|
|
301
|
+
printData(result, wantsJson, publicPreviewLines(result));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
case "export": {
|
|
305
|
+
const eventIds = takeRepeatedOptions(rest, "--event");
|
|
306
|
+
rejectArgs(rest, USAGE);
|
|
307
|
+
const result = exportLocalPublicEvidence({ eventIds }, configDir);
|
|
308
|
+
printData(result, wantsJson, publicExportLines(result));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
case "verify": {
|
|
312
|
+
const path = takeOption(rest, "--file");
|
|
313
|
+
if (!path) throw new CliUsageError("public verify requires --file", USAGE);
|
|
314
|
+
rejectArgs(rest, USAGE);
|
|
315
|
+
const result = verifyPublicEvidenceFile(path);
|
|
316
|
+
printData(result, wantsJson, publicVerificationLines(result));
|
|
317
|
+
if (result.status !== "cryptographically_valid") {
|
|
318
|
+
throw new RuntimeApiError(`public evidence verification failed: ${result.status}`, 422, result);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
case "import": {
|
|
323
|
+
const path = takeOption(rest, "--file");
|
|
324
|
+
if (!path) throw new CliUsageError("public import requires --file", USAGE);
|
|
325
|
+
rejectArgs(rest, USAGE);
|
|
326
|
+
const result = importCommunityEvidenceFile(path, configDir);
|
|
327
|
+
printData(result, wantsJson, [
|
|
328
|
+
`Community evidence imported: ${result.bundleId}`,
|
|
329
|
+
`Publisher: ${result.publisherKeyId}`,
|
|
330
|
+
"Trust: community_untrusted_v1; not locally verified.",
|
|
331
|
+
]);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
case "community": {
|
|
335
|
+
rejectArgs(rest, USAGE);
|
|
336
|
+
const result = listCommunityEvidenceContext(configDir);
|
|
337
|
+
const lines = result.evidence.length > 0
|
|
338
|
+
? result.evidence.map((row) =>
|
|
339
|
+
`${row.bundleId} publisher=${row.publisherKeyId} active=${row.activeRecordCount} revoked=${row.revokedRecordCount}`,
|
|
340
|
+
)
|
|
341
|
+
: ["No community evidence"];
|
|
342
|
+
printData(result, wantsJson, [
|
|
343
|
+
"Community evidence (untrusted, read-only context; not locally verified)",
|
|
344
|
+
...lines,
|
|
345
|
+
]);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
default:
|
|
349
|
+
throw new CliUsageError("unknown public subcommand", USAGE);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
226
353
|
export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): Promise<number> {
|
|
227
354
|
return runCliAction(async () => {
|
|
228
355
|
const configDir = deps.configDir ?? getConfigDir();
|
|
@@ -232,6 +359,10 @@ export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): P
|
|
|
232
359
|
|
|
233
360
|
try {
|
|
234
361
|
switch (sub) {
|
|
362
|
+
case "public": {
|
|
363
|
+
handlePublicLabCommand(rest, wantsJson, configDir);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
235
366
|
case "status": {
|
|
236
367
|
rejectArgs(rest, USAGE);
|
|
237
368
|
const status = queryLabStatus(configDir);
|
|
@@ -243,6 +374,9 @@ export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): P
|
|
|
243
374
|
const limit = takeIntegerOption(rest, "--limit", { min: 1 });
|
|
244
375
|
rejectArgs(rest, USAGE);
|
|
245
376
|
if (!subjectId) throw new CliUsageError("--subject is required", USAGE);
|
|
377
|
+
if (!isLabRouteSubjectId(subjectId)) {
|
|
378
|
+
throw new CliUsageError("--subject must be an exact Lab route subject id", USAGE);
|
|
379
|
+
}
|
|
246
380
|
const result = queryPassiveProductionSignals(subjectId, limit, configDir);
|
|
247
381
|
printData(result, wantsJson, passiveProductionLines(result));
|
|
248
382
|
return;
|
|
@@ -461,7 +595,7 @@ export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): P
|
|
|
461
595
|
throw new CliUsageError(`unknown lab subcommand: ${sub}`, USAGE);
|
|
462
596
|
}
|
|
463
597
|
} catch (err) {
|
|
464
|
-
if (err instanceof CliUsageError) throw err;
|
|
598
|
+
if (err instanceof CliUsageError || err instanceof RuntimeApiError) throw err;
|
|
465
599
|
if (err instanceof LabProjectionUnavailableError || err instanceof LabProjectionIncompatibleError) {
|
|
466
600
|
throw new LabStateError(labErrorMessage(err));
|
|
467
601
|
}
|