@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.
Files changed (130) hide show
  1. package/gui/dist/assets/index-BOFeam5a.js +76 -0
  2. package/gui/dist/assets/index-Xq49CY8F.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic.ts +2 -28
  6. package/src/adapters/google.ts +31 -3
  7. package/src/adapters/mimo-free.ts +17 -0
  8. package/src/adapters/openai-chat.ts +392 -40
  9. package/src/adapters/registry.ts +144 -0
  10. package/src/adapters/responses-tool-schema.ts +67 -0
  11. package/src/bridge.ts +15 -2
  12. package/src/chat/inbound.ts +13 -7
  13. package/src/claude/gateway-cache.ts +41 -4
  14. package/src/cli/claude.ts +3 -2
  15. package/src/cli/combo.ts +3 -0
  16. package/src/cli/dispatch.ts +8 -0
  17. package/src/cli/export-command.ts +2 -2
  18. package/src/cli/help.ts +2 -0
  19. package/src/cli/index.ts +3 -2
  20. package/src/cli/lab.ts +135 -1
  21. package/src/cli/minimax.ts +491 -0
  22. package/src/cli/models-runtime.ts +22 -1
  23. package/src/cli/models.ts +67 -2
  24. package/src/cli/opencode.ts +2 -1
  25. package/src/cli/registry.ts +22 -2
  26. package/src/clients/config-export.ts +125 -7
  27. package/src/codex/app-server-processes.ts +57 -2
  28. package/src/codex/app-server-restart-service.ts +232 -0
  29. package/src/codex/catalog/aggregation.ts +10 -1
  30. package/src/codex/catalog/effort.ts +15 -3
  31. package/src/codex/catalog/parsing.ts +3 -1
  32. package/src/codex/catalog/provider-fetch.ts +45 -5
  33. package/src/codex/catalog/sync.ts +74 -4
  34. package/src/codex/convergence.ts +2 -0
  35. package/src/combos/index.ts +1 -0
  36. package/src/combos/request.ts +30 -0
  37. package/src/combos/types.ts +6 -0
  38. package/src/config.ts +52 -0
  39. package/src/generated/compatibility-version.json +241 -85
  40. package/src/images/loop.ts +26 -6
  41. package/src/integrations/registry.ts +7 -0
  42. package/src/lab/conformance/jcs.ts +42 -2
  43. package/src/lab/conformance/negative-controls.ts +6 -2
  44. package/src/lab/conformance/runner.ts +16 -5
  45. package/src/lab/fabric/observe.ts +49 -14
  46. package/src/lab/index.ts +16 -0
  47. package/src/lab/ledger/purge.ts +152 -83
  48. package/src/lab/ledger/store.ts +168 -54
  49. package/src/lab/observe/from-conformance.ts +8 -6
  50. package/src/lab/observe/from-live.ts +8 -2
  51. package/src/lab/paths.ts +23 -0
  52. package/src/lab/public/bundle.ts +217 -0
  53. package/src/lab/public/community-authority.ts +175 -0
  54. package/src/lab/public/community-files.ts +29 -0
  55. package/src/lab/public/community.ts +479 -0
  56. package/src/lab/public/file-safety.ts +155 -0
  57. package/src/lab/public/ids.ts +26 -0
  58. package/src/lab/public/index.ts +16 -0
  59. package/src/lab/public/mutation-lock.ts +424 -0
  60. package/src/lab/public/operator.ts +353 -0
  61. package/src/lab/public/origin-purge.ts +79 -0
  62. package/src/lab/public/origin.ts +203 -0
  63. package/src/lab/public/privacy.ts +143 -0
  64. package/src/lab/public/private-file.ts +261 -0
  65. package/src/lab/public/project.ts +124 -0
  66. package/src/lab/public/purge-test-fault.ts +21 -0
  67. package/src/lab/public/purge.ts +223 -0
  68. package/src/lab/public/registry.ts +44 -0
  69. package/src/lab/public/revocation.ts +252 -0
  70. package/src/lab/public/signature.ts +219 -0
  71. package/src/lab/public/storage.ts +105 -0
  72. package/src/lab/public/strict-json.ts +206 -0
  73. package/src/lab/public/time.ts +26 -0
  74. package/src/lab/public/types.ts +172 -0
  75. package/src/lab/public/validate.ts +391 -0
  76. package/src/lib/codex-restart-contract.ts +120 -0
  77. package/src/lib/lab-activation.ts +109 -47
  78. package/src/lib/lab-live-pinned-sender.ts +16 -5
  79. package/src/lib/pinned-http.ts +70 -16
  80. package/src/lib/self-launch-argv.ts +15 -0
  81. package/src/lib/state-store-registrations.ts +2 -0
  82. package/src/lib/upstream-reachability.ts +4 -0
  83. package/src/lib/windows-elevation.ts +10 -1
  84. package/src/providers/derive.ts +24 -4
  85. package/src/providers/registry.ts +14 -4
  86. package/src/providers/request-pacing.ts +310 -0
  87. package/src/providers/service-tier.ts +143 -0
  88. package/src/providers/static-model-discovery.ts +86 -0
  89. package/src/reasoning-effort.ts +27 -1
  90. package/src/responses/custom-tool-compat.ts +4 -1
  91. package/src/responses/parser.ts +7 -1
  92. package/src/responses/provider-opaque-metadata.ts +73 -0
  93. package/src/responses/schema.ts +6 -0
  94. package/src/router.ts +23 -6
  95. package/src/routing/capability.ts +4 -2
  96. package/src/routing/compatibility/behavior.ts +5 -1
  97. package/src/server/adapter-resolve.ts +2 -32
  98. package/src/server/auth-cors.ts +50 -6
  99. package/src/server/chat-completions.ts +74 -36
  100. package/src/server/chat-native-sse.ts +331 -0
  101. package/src/server/chat-native.ts +371 -0
  102. package/src/server/management/combo-routes.ts +16 -2
  103. package/src/server/management/config-routes.ts +6 -4
  104. package/src/server/management/context.ts +17 -0
  105. package/src/server/management/lab-routes.ts +181 -19
  106. package/src/server/management/model-routes.ts +76 -2
  107. package/src/server/management/model-rows.ts +8 -0
  108. package/src/server/management/provider-capability-config.ts +48 -0
  109. package/src/server/management/provider-routes.ts +76 -4
  110. package/src/server/management/system-restart.ts +4 -2
  111. package/src/server/management/system-routes.ts +38 -0
  112. package/src/server/relay.ts +17 -3
  113. package/src/server/responses/compact.ts +4 -1
  114. package/src/server/responses/core.ts +257 -46
  115. package/src/server/responses/empty-completion-guard.ts +276 -0
  116. package/src/server/responses/fetch-helpers.ts +35 -4
  117. package/src/server/responses/pacing-overload.ts +13 -0
  118. package/src/server/responses/policy-fallback.ts +16 -2
  119. package/src/server/responses/terminal-guard.ts +1 -1
  120. package/src/server/responses/upstream-error.ts +5 -0
  121. package/src/server/responses.ts +17 -2
  122. package/src/server/system-env.ts +1 -1
  123. package/src/types.ts +84 -5
  124. package/src/update/index.ts +6 -5
  125. package/src/update/job.ts +5 -6
  126. package/src/update/notify.ts +5 -3
  127. package/src/usage/log.ts +11 -1
  128. package/src/web-search/loop.ts +21 -5
  129. package/gui/dist/assets/index-CQ7bIKee.css +0 -1
  130. 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
  {
@@ -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
- if (!isRec(raw)) throw new ChatCompletionsRequestError("request body must be a JSON object");
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(port: number, timeoutMs = 3_000, configDir?: string): Promise<string | null> {
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: { "anthropic-version": "2023-06-01" },
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, [process.argv[1], "start", "--port", String(pinPort)], {
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 } : {}) }),
@@ -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
- * Seven clients, four formats: opencode and Pi are JSON, OMP, Hermes and Gajae
5
- * are YAML, OpenClaw JSON5, Kimi TOML.
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 = [process.argv[1], "start"];
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
  }