@bitkyc08/opencodex 2.7.42 → 2.7.43

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 (143) hide show
  1. package/README.md +8 -1
  2. package/bin/ocx.mjs +13 -14
  3. package/gui/dist/assets/index-Czw-jpTU.css +1 -0
  4. package/gui/dist/assets/index-cmds12BG.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  10. package/src/adapters/cursor/protobuf-events.ts +24 -2
  11. package/src/adapters/cursor/protobuf-request.ts +1 -2
  12. package/src/adapters/cursor/tool-definitions.ts +68 -29
  13. package/src/adapters/google-wire-compiler.ts +4 -0
  14. package/src/adapters/google.ts +128 -2
  15. package/src/adapters/identity.ts +12 -2
  16. package/src/adapters/kiro.ts +49 -6
  17. package/src/adapters/mimo-free.ts +2 -0
  18. package/src/adapters/openai-responses.ts +246 -59
  19. package/src/claude/agents-inject.ts +5 -0
  20. package/src/claude/inbound.ts +26 -9
  21. package/src/cli/account-auth.ts +1 -1
  22. package/src/cli/agent-driven.ts +37 -0
  23. package/src/cli/claude.ts +3 -3
  24. package/src/cli/doctor.ts +23 -18
  25. package/src/cli/help.ts +42 -6
  26. package/src/cli/index.ts +88 -19
  27. package/src/cli/interactive-confirm.ts +129 -0
  28. package/src/cli/opencode.ts +701 -0
  29. package/src/cli/provider-runtime.ts +3 -0
  30. package/src/cli/provider.ts +31 -10
  31. package/src/cli/star-prompt.ts +55 -16
  32. package/src/cli/status.ts +47 -13
  33. package/src/codex/account-id.ts +34 -0
  34. package/src/codex/account-lifecycle.ts +4 -1
  35. package/src/codex/account-namespace-match.ts +63 -0
  36. package/src/codex/account-namespaces.ts +149 -0
  37. package/src/codex/account-pause.ts +20 -0
  38. package/src/codex/account-usability.ts +6 -1
  39. package/src/codex/app-server-processes.ts +511 -0
  40. package/src/codex/auth-api.ts +293 -34
  41. package/src/codex/auth-collision.ts +2 -1
  42. package/src/codex/auth-context.ts +60 -17
  43. package/src/codex/catalog/parsing.ts +16 -1
  44. package/src/codex/catalog/provider-fetch.ts +48 -12
  45. package/src/codex/catalog/sync.ts +18 -3
  46. package/src/codex/catalog.ts +1 -1
  47. package/src/codex/history-provider.ts +145 -1
  48. package/src/codex/inject.ts +114 -14
  49. package/src/codex/main-account.ts +2 -8
  50. package/src/codex/pool-rotation.ts +186 -0
  51. package/src/codex/quota.ts +15 -0
  52. package/src/codex/routing.ts +695 -106
  53. package/src/codex/shim.ts +4 -1
  54. package/src/codex/subagent-defaults.ts +550 -0
  55. package/src/codex/subagent-model-fallback.ts +2 -0
  56. package/src/codex/sync.ts +3 -0
  57. package/src/config.ts +566 -25
  58. package/src/images/artifacts.ts +516 -0
  59. package/src/images/fulfill-video.ts +163 -0
  60. package/src/images/fulfill.ts +111 -0
  61. package/src/images/index.ts +4 -0
  62. package/src/images/loop.ts +789 -0
  63. package/src/images/plan.ts +133 -0
  64. package/src/images/synthetic-tool.ts +133 -0
  65. package/src/images/types.ts +41 -0
  66. package/src/images/xai-client.ts +141 -0
  67. package/src/images/xai-video-client.ts +163 -0
  68. package/src/lib/admin-secrets.ts +25 -0
  69. package/src/lib/config-ownership.ts +327 -0
  70. package/src/lib/crash-guard.ts +2 -0
  71. package/src/lib/destination-policy.ts +122 -4
  72. package/src/lib/pinned-http.ts +151 -0
  73. package/src/lib/process-control.ts +2 -2
  74. package/src/lib/provider-outbound.ts +164 -0
  75. package/src/lib/provider-url.ts +14 -0
  76. package/src/lib/proxy-env.ts +18 -0
  77. package/src/lib/windows-elevation.ts +81 -3
  78. package/src/lib/windows-secret-acl.ts +189 -12
  79. package/src/lib/winsw.ts +2 -0
  80. package/src/oauth/anthropic-routing.ts +570 -0
  81. package/src/oauth/health.ts +6 -0
  82. package/src/oauth/index.ts +283 -72
  83. package/src/oauth/key-providers.ts +17 -6
  84. package/src/oauth/kimi.ts +2 -0
  85. package/src/oauth/kiro-credentials.ts +322 -10
  86. package/src/oauth/kiro.ts +423 -43
  87. package/src/oauth/login-cli.ts +32 -5
  88. package/src/oauth/store.ts +54 -4
  89. package/src/oauth/types.ts +11 -0
  90. package/src/providers/alibaba-region-migration.ts +16 -3
  91. package/src/providers/antigravity-models.ts +3 -0
  92. package/src/providers/api-keys.ts +13 -6
  93. package/src/providers/derive.ts +6 -0
  94. package/src/providers/key-failover.ts +24 -4
  95. package/src/providers/quota.ts +233 -29
  96. package/src/providers/registry.ts +11 -3
  97. package/src/responses/parser.ts +11 -0
  98. package/src/responses/state.ts +22 -8
  99. package/src/responses/tool-groups.ts +19 -0
  100. package/src/router.ts +14 -4
  101. package/src/server/auth-cors.ts +110 -22
  102. package/src/server/claude-messages.ts +8 -1
  103. package/src/server/gui-static.ts +30 -6
  104. package/src/server/images.ts +303 -9
  105. package/src/server/index.ts +77 -9
  106. package/src/server/lifecycle.ts +25 -1
  107. package/src/server/management/agent-settings-routes.ts +28 -4
  108. package/src/server/management/combo-routes.ts +7 -0
  109. package/src/server/management/config-routes.ts +5 -2
  110. package/src/server/management/logs-usage-routes.ts +167 -3
  111. package/src/server/management/oauth-account-routes.ts +163 -17
  112. package/src/server/management/provider-routes.ts +35 -7
  113. package/src/server/management/system-restart.ts +172 -0
  114. package/src/server/management/system-routes.ts +33 -10
  115. package/src/server/management-api.ts +2 -2
  116. package/src/server/management-auth.ts +216 -0
  117. package/src/server/proxy-liveness.ts +14 -3
  118. package/src/server/responses/compact.ts +21 -13
  119. package/src/server/responses/core.ts +583 -152
  120. package/src/server/responses-image-gen-repair.ts +118 -0
  121. package/src/server/responses-item-id-repair.ts +10 -85
  122. package/src/server/sse-payload-rewrite.ts +116 -0
  123. package/src/server/system-env.ts +28 -10
  124. package/src/service.ts +49 -2
  125. package/src/storage/cleanup-job.ts +57 -0
  126. package/src/storage/cleanup.ts +1504 -28
  127. package/src/storage/policy-job.ts +366 -0
  128. package/src/storage/policy-scheduler.ts +40 -0
  129. package/src/storage/policy-worker.ts +53 -0
  130. package/src/storage/policy.ts +522 -0
  131. package/src/storage/restore-job.ts +242 -0
  132. package/src/storage/restore-worker.ts +52 -0
  133. package/src/storage/storage-mutation-coordinator.ts +109 -0
  134. package/src/tray/windows.ts +2 -0
  135. package/src/types.ts +96 -1
  136. package/src/update/index.ts +36 -18
  137. package/src/update/job.ts +103 -12
  138. package/src/update/npm-invocation.d.mts +23 -0
  139. package/src/update/npm-invocation.mjs +94 -0
  140. package/src/usage/debug.ts +2 -0
  141. package/src/usage/log.ts +4 -0
  142. package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
  143. package/gui/dist/assets/index-DfVGuN88.css +0 -1
@@ -52,6 +52,24 @@ const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantRespons
52
52
  const SDK_VERSION = "1.0.27";
53
53
  const NODE_VERSION = "22.21.1";
54
54
  const KIRO_IDE_VERSION = "1.0.0";
55
+ type KiroWireClient = "ide" | "cli";
56
+
57
+ function kiroCliPlatform(): "linux" | "macos" | "windows" {
58
+ return process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
59
+ }
60
+
61
+ function kiroCliUserAgent(includeAppVersion: boolean): string {
62
+ return [
63
+ "aws-sdk-rust/1.3.15",
64
+ "ua/2.1",
65
+ "api/codewhispererstreaming/0.1.17975",
66
+ `os/${kiroCliPlatform()}`,
67
+ "lang/rust/1.92.0",
68
+ ...(includeAppVersion ? ["md/appVersion-2.14.2"] : []),
69
+ "m/F",
70
+ "app/AmazonQ-For-CLI",
71
+ ].join(" ");
72
+ }
55
73
 
56
74
  // Payload construction (conversationState)
57
75
  interface KiroToolUse {
@@ -68,7 +86,10 @@ interface KiroUserInputMessage {
68
86
  content: string;
69
87
  modelId?: string;
70
88
  origin?: string;
71
- userInputMessageContext?: { tools?: unknown[]; toolResults?: KiroToolResult[] };
89
+ userInputMessageContext?: {
90
+ tools?: unknown[];
91
+ toolResults?: KiroToolResult[];
92
+ };
72
93
  images?: KiroImage[];
73
94
  }
74
95
  interface KiroHistoryEntry {
@@ -385,6 +406,7 @@ export function buildKiroPayload(
385
406
  parsed: OcxParsedRequest,
386
407
  profileArn: string | undefined,
387
408
  forcedCompletionMode?: KiroCompletionMode,
409
+ wireClient: KiroWireClient = "ide",
388
410
  ): {
389
411
  payload: Record<string, unknown>;
390
412
  nameMap: Map<string, string>;
@@ -509,7 +531,7 @@ export function buildKiroPayload(
509
531
  userInputMessage: {
510
532
  content: turn.content,
511
533
  modelId,
512
- origin: "AI_EDITOR",
534
+ origin: wireClient === "cli" ? "KIRO_CLI" : "AI_EDITOR",
513
535
  ...(turn.images.length > 0 ? { images: turn.images } : {}),
514
536
  ...(turn.toolResults.length > 0 ? { userInputMessageContext: { toolResults: turn.toolResults } } : {}),
515
537
  },
@@ -539,6 +561,10 @@ export function buildKiroPayload(
539
561
  const payload: Record<string, unknown> = {
540
562
  conversationState: {
541
563
  chatTriggerType: "MANUAL",
564
+ ...(wireClient === "cli" ? {
565
+ agentContinuationId: crypto.randomUUID(),
566
+ agentTaskType: "vibe",
567
+ } : {}),
542
568
  conversationId,
543
569
  currentMessage: { userInputMessage: currentUim },
544
570
  ...(history.length > 0 ? { history } : {}),
@@ -1445,10 +1471,26 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1445
1471
  if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
1446
1472
  throw new Error("kiro token missing — run ocx login kiro");
1447
1473
  }
1448
- const region = resolveKiroApiRegion();
1449
- const profileArn = resolveKiroProfileArn();
1474
+ const region = resolveKiroApiRegion(parsed._kiroAuthContext);
1475
+ const resolvedProfileArn = resolveKiroProfileArn(parsed._kiroAuthContext);
1476
+ const isApiKey = provider.apiKey.trim().startsWith("ksk_");
1477
+ const profileArn = isApiKey ? undefined : resolvedProfileArn;
1478
+ // Builder ID and Kiro API keys have no profile ARN and are accepted only on Kiro's CLI
1479
+ // request path. Enterprise profiles retain the existing IDE-shaped request.
1480
+ const wireClient: KiroWireClient = isApiKey || !profileArn ? "cli" : "ide";
1450
1481
  const fp = fingerprint().slice(0, 64);
1451
- const headers: Record<string, string> = {
1482
+ const headers: Record<string, string> = wireClient === "cli" ? {
1483
+ authorization: `Bearer ${provider.apiKey}`,
1484
+ "content-type": "application/x-amz-json-1.0",
1485
+ accept: "*/*",
1486
+ "x-amz-target": AMZ_TARGET,
1487
+ "user-agent": kiroCliUserAgent(true),
1488
+ "x-amz-user-agent": kiroCliUserAgent(false),
1489
+ "x-amzn-codewhisperer-optout": "true",
1490
+ "amz-sdk-request": "attempt=1; max=3",
1491
+ "amz-sdk-invocation-id": invocationId(),
1492
+ ...(isApiKey ? { tokentype: "API_KEY" } : {}),
1493
+ } : {
1452
1494
  authorization: `Bearer ${provider.apiKey}`,
1453
1495
  "content-type": "application/x-amz-json-1.0",
1454
1496
  accept: "application/vnd.amazon.eventstream",
@@ -1460,7 +1502,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1460
1502
  "amz-sdk-invocation-id": invocationId(),
1461
1503
  };
1462
1504
  if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
1463
- const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode);
1505
+ const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode, wireClient);
1464
1506
  await normalizeKiroImages(built.payload);
1465
1507
  const contextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
1466
1508
  const body = JSON.stringify(built.payload);
@@ -1472,6 +1514,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1472
1514
  messageCount: kiroPayloadMessages(parsed).length,
1473
1515
  toolCount: parsed.context.tools?.length ?? 0,
1474
1516
  hasProfileArn: Boolean(profileArn),
1517
+ wireClient,
1475
1518
  hasPreviousResponseId: Boolean(parsed.previousResponseId),
1476
1519
  });
1477
1520
  return {
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { getConfigDir } from "../config";
5
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
5
6
  import type { OcxProviderConfig, OcxParsedRequest } from "../types";
6
7
  import { createOpenAIChatAdapter } from "./openai-chat";
7
8
  import type { ProviderAdapter, AdapterRequest } from "./base";
@@ -59,6 +60,7 @@ export function getMimoClientId(): string {
59
60
  } catch { /* fall through to regenerate */ }
60
61
  const fresh = randomUUID();
61
62
  try {
63
+ recordOwnedConfigPath(dir, file);
62
64
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
63
65
  writeFileSync(file, `${fresh}\n`, "utf8");
64
66
  } catch { /* persist best-effort; still usable for this process */ }
@@ -1,8 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { IncomingMeta, ProviderAdapter } from "./base";
3
- import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
3
+ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage } from "../types";
4
4
  import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
5
5
  import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
6
+ import { collectResponsesToolGroups } from "../responses/tool-groups";
6
7
  import { decodeServerSentEvents } from "../lib/sse-decoder";
7
8
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
8
9
  import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
@@ -500,72 +501,252 @@ function stripUnsupportedForwardParams(body: unknown): unknown {
500
501
  return rest;
501
502
  }
502
503
 
504
+ const IMAGE_GEN_NAMESPACE = "image_gen";
505
+ const HOSTED_IMAGE_GENERATION_TOOL = "image_generation";
506
+ const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
507
+ const IMAGE_GEN_WIRE_PREFIX = `${IMAGE_GEN_NAMESPACE}__`;
508
+
509
+ /** Remove a supported client prefix before constructing the canonical image-gen wire alias. */
510
+ function imageGenLocalName(name: string): string {
511
+ if (name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) return name.slice(IMAGE_GEN_DOTTED_PREFIX.length);
512
+ if (name.startsWith(IMAGE_GEN_WIRE_PREFIX)) return name.slice(IMAGE_GEN_WIRE_PREFIX.length);
513
+ return name;
514
+ }
515
+
516
+ /** Build the flat public-Responses name used only on the upstream wire. */
517
+ function imageGenWireName(name: string): string {
518
+ return namespacedToolName(IMAGE_GEN_NAMESPACE, imageGenLocalName(name));
519
+ }
520
+
521
+ /** Match client image-gen declarations across namespace, legacy dotted, and canonical wire forms. */
522
+ function isImageGenClientName(name: string): boolean {
523
+ return name === IMAGE_GEN_NAMESPACE
524
+ || name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
525
+ || name.startsWith(IMAGE_GEN_WIRE_PREFIX);
526
+ }
527
+
528
+ /** Identify declarations that should activate image-gen request normalization. */
529
+ function declaresImageGenClientTool(tool: unknown): boolean {
530
+ if (!isPlainObject(tool) || typeof tool.name !== "string") return false;
531
+ if (tool.type === "namespace") return tool.name === IMAGE_GEN_NAMESPACE;
532
+ return isImageGenClientName(tool.name);
533
+ }
534
+
503
535
  /**
504
- * Hosted tool types whose server-side function names collide with the client tools Codex
505
- * declares for the matching app skill. Codex sends BOTH (e.g. hosted `image_generation` plus a
506
- * declared `image_gen.imagegen` function/namespace tool for the imagegen skill). The ChatGPT
507
- * backend tolerates the pair, but the platform `/v1/responses` rejects it:
508
- * `Invalid Value: 'tools'. Function 'image_gen.imagegen' conflicts with a hosted tool in the
509
- * same request.` Keyed hosted-type conflicting client tool-name prefix; the hosted entry is
510
- * dropped (the declared tool wins Codex executes the skill client-side either way).
536
+ * Lower one complete Codex image-gen namespace to public Responses function tools.
537
+ *
538
+ * The public API reserves the `image_gen` namespace and restricts function names to a flat safe
539
+ * alphabet. `image_gen__<tool>` is therefore an upstream-only alias; client-facing responses are
540
+ * restored to explicit `{ namespace: "image_gen", name: "<tool>" }` calls by the server. Only a
541
+ * non-empty namespace containing named function tools is safe to lower. Malformed, empty, and
542
+ * future namespace shapes stay untouched instead of silently losing client capabilities.
511
543
  */
512
- const HOSTED_TOOL_NAME_CONFLICTS: ReadonlyArray<{ hostedType: string; namePrefix: string }> = [
513
- { hostedType: "image_generation", namePrefix: "image_gen" },
514
- ];
544
+ function flattenImageGenNamespace(tool: unknown): Record<string, unknown>[] | undefined {
545
+ if (
546
+ !isPlainObject(tool)
547
+ || tool.type !== "namespace"
548
+ || tool.name !== IMAGE_GEN_NAMESPACE
549
+ || !Array.isArray(tool.tools)
550
+ || tool.tools.length === 0
551
+ ) return undefined;
552
+
553
+ for (const innerTool of tool.tools) {
554
+ if (
555
+ !isPlainObject(innerTool)
556
+ || innerTool.type !== "function"
557
+ || typeof innerTool.name !== "string"
558
+ || innerTool.name.length === 0
559
+ ) return undefined;
560
+ }
561
+
562
+ return tool.tools.map(innerTool => {
563
+ const functionTool = innerTool as Record<string, unknown> & { name: string };
564
+ return {
565
+ ...functionTool,
566
+ name: imageGenWireName(functionTool.name),
567
+ };
568
+ });
569
+ }
570
+
571
+ /** Convert a legacy dotted function declaration while preserving all other function metadata. */
572
+ function normalizeFlatImageGenFunction(tool: unknown): unknown {
573
+ if (
574
+ !isPlainObject(tool)
575
+ || tool.type !== "function"
576
+ || typeof tool.name !== "string"
577
+ || !tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
578
+ ) return tool;
579
+ return { ...tool, name: imageGenWireName(tool.name) };
580
+ }
581
+
582
+ /** Return the image-gen function name used for stable cross-container deduplication. */
583
+ function imageGenFunctionName(tool: unknown): string | undefined {
584
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
585
+ return undefined;
586
+ }
587
+ return isImageGenClientName(tool.name) ? tool.name : undefined;
588
+ }
589
+
590
+ /** True only when a declaration can yield a callable upstream-safe image-gen function alias. */
591
+ function declaresUsableImageGenAlias(tool: unknown): boolean {
592
+ if (flattenImageGenNamespace(tool)) return true;
593
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
594
+ return false;
595
+ }
596
+ if (tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
597
+ return tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length;
598
+ }
599
+ return tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
600
+ && tool.name.length > IMAGE_GEN_WIRE_PREFIX.length;
601
+ }
602
+
603
+ /** Collect client tool-choice names and the exact upstream aliases declared for them. */
604
+ function imageGenToolChoiceAliases(toolGroups: unknown[][]): Map<string, string> {
605
+ const aliases = new Map<string, string>();
606
+
607
+ for (const group of toolGroups) {
608
+ for (const tool of group) {
609
+ const flattened = flattenImageGenNamespace(tool);
610
+ if (flattened) {
611
+ for (const candidate of flattened) {
612
+ const wireName = candidate.name as string;
613
+ aliases.set(`${IMAGE_GEN_DOTTED_PREFIX}${imageGenLocalName(wireName)}`, wireName);
614
+ aliases.set(wireName, wireName);
615
+ }
616
+ continue;
617
+ }
618
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
619
+ continue;
620
+ }
621
+ if (
622
+ tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
623
+ && tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length
624
+ ) {
625
+ aliases.set(tool.name, imageGenWireName(tool.name));
626
+ } else if (
627
+ tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
628
+ && tool.name.length > IMAGE_GEN_WIRE_PREFIX.length
629
+ ) {
630
+ aliases.set(tool.name, tool.name);
631
+ }
632
+ }
633
+ }
634
+
635
+ return aliases;
636
+ }
637
+
638
+ /** Rewrite function selectors only when their corresponding declaration receives a wire alias. */
639
+ function normalizeImageGenToolChoice(
640
+ toolChoice: unknown,
641
+ aliases: ReadonlyMap<string, string>,
642
+ ): unknown {
643
+ if (!isPlainObject(toolChoice)) return toolChoice;
644
+
645
+ if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
646
+ const alias = aliases.get(toolChoice.name);
647
+ return alias && alias !== toolChoice.name ? { ...toolChoice, name: alias } : toolChoice;
648
+ }
649
+
650
+ if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
651
+ let changed = false;
652
+ const tools = toolChoice.tools.map(tool => {
653
+ if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
654
+ return tool;
655
+ }
656
+ const alias = aliases.get(tool.name);
657
+ if (!alias || alias === tool.name) return tool;
658
+ changed = true;
659
+ return { ...tool, name: alias };
660
+ });
661
+ return changed ? { ...toolChoice, tools } : toolChoice;
662
+ }
663
+
664
+ /** Identify replayed image-gen calls that require upstream wire encoding. */
665
+ function declaresImageGenFunctionCall(item: unknown): boolean {
666
+ if (!isPlainObject(item) || item.type !== "function_call" || typeof item.name !== "string") {
667
+ return false;
668
+ }
669
+ return item.namespace === IMAGE_GEN_NAMESPACE || isImageGenClientName(item.name);
670
+ }
671
+
672
+ /** Encode native or legacy replay calls to the same flat name used by tool declarations. */
673
+ function normalizeImageGenFunctionCall(item: unknown): unknown {
674
+ if (!declaresImageGenFunctionCall(item) || !isPlainObject(item) || typeof item.name !== "string") {
675
+ return item;
676
+ }
677
+ if (item.namespace === IMAGE_GEN_NAMESPACE) {
678
+ const { namespace: _namespace, ...rest } = item;
679
+ return { ...rest, name: imageGenWireName(item.name) };
680
+ }
681
+ if (item.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
682
+ return { ...item, name: imageGenWireName(item.name) };
683
+ }
684
+ return item;
685
+ }
515
686
 
516
687
  /**
517
- * Drop hosted tools whose names collide with declared function/namespace tools (see
518
- * HOSTED_TOOL_NAME_CONFLICTS). Only applies on the API-key platform path: the ChatGPT backend
519
- * ("forward" mode) accepts the pair, and stripping there would disable native imagegen. No-op
520
- * (returns the original reference) when nothing matches.
688
+ * Normalize Codex's private image-gen tool declaration for API-key Responses providers.
521
689
  *
522
- * Codex Desktop Responses Lite requests do not always use top-level `body.tools`; they may place
523
- * the same declarations in `body.input[]` entries of type `additional_tools`. The platform validates
524
- * these containers as one tool namespace, so conflict detection must span all of them. The function
525
- * uses copy-on-write semantics and preserves the original request reference when nothing is removed.
690
+ * A complete `image_gen` namespace is flattened to safe `image_gen__<tool>` aliases even when it is
691
+ * the only image tool in the request. Replayed client calls are encoded to the same alias, including
692
+ * legacy dotted calls from older compatibility attempts. When a usable alias replaces a client
693
+ * image-gen declaration, the duplicate hosted `image_generation` entry is removed. Duplicate aliases
694
+ * are resolved in stable container order: top-level tools first, then Responses Lite
695
+ * `additional_tools` entries.
696
+ *
697
+ * This function is called only on the API-key path. ChatGPT forward mode understands the private
698
+ * namespace and must keep it. Copy-on-write preserves the original request reference when no
699
+ * namespace is flattened, hosted tool removed, or duplicate function discarded.
526
700
  */
527
- function stripConflictingHostedTools(body: unknown): unknown {
701
+ function normalizeImageGenClientTools(body: unknown): unknown {
528
702
  if (!isPlainObject(body)) return body;
529
703
 
530
- // Collect every tool container in the request. Traditional HTTP/SSE requests use top-level
531
- // `tools`, while Responses Lite may carry the same declarations in `additional_tools` entries.
532
- const toolGroups: unknown[][] = [];
533
- if (Array.isArray(body.tools)) toolGroups.push(body.tools);
534
- if (Array.isArray(body.input)) {
535
- for (const item of body.input) {
536
- if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
537
- toolGroups.push(item.tools);
704
+ const toolGroups = collectResponsesToolGroups(body);
705
+ const hasImageGenClientTool = toolGroups.some(group => group.some(declaresImageGenClientTool))
706
+ || (Array.isArray(body.input) && body.input.some(declaresImageGenFunctionCall));
707
+ if (!hasImageGenClientTool) return body;
708
+ const hasUsableImageGenAlias = toolGroups.some(group => group.some(declaresUsableImageGenAlias));
709
+ const toolChoiceAliases = imageGenToolChoiceAliases(toolGroups);
710
+
711
+ const seenFunctionNames = new Set<string>();
712
+ const normalizeGroup = (tools: unknown[]): unknown[] => {
713
+ const normalized: unknown[] = [];
714
+ let groupChanged = false;
715
+
716
+ for (const tool of tools) {
717
+ if (
718
+ hasUsableImageGenAlias
719
+ && isPlainObject(tool)
720
+ && tool.type === HOSTED_IMAGE_GENERATION_TOOL
721
+ ) {
722
+ groupChanged = true;
723
+ continue;
724
+ }
725
+
726
+ const flattened = flattenImageGenNamespace(tool);
727
+ const candidates = flattened ?? [tool];
728
+ if (flattened) groupChanged = true;
729
+
730
+ for (const candidate of candidates) {
731
+ const normalizedCandidate = normalizeFlatImageGenFunction(candidate);
732
+ if (normalizedCandidate !== candidate) groupChanged = true;
733
+ const functionName = imageGenFunctionName(normalizedCandidate);
734
+ if (functionName && seenFunctionNames.has(functionName)) {
735
+ groupChanged = true;
736
+ continue;
737
+ }
738
+ if (functionName) seenFunctionNames.add(functionName);
739
+ normalized.push(normalizedCandidate);
538
740
  }
539
741
  }
540
- }
541
- if (toolGroups.length === 0) return body;
542
-
543
- // Detect conflicts across containers because the client namespace and the hosted tool may be
544
- // declared in different groups while still sharing one platform-validated namespace.
545
- const conflicting = HOSTED_TOOL_NAME_CONFLICTS.filter(c => toolGroups.some(group =>
546
- group.some(t => {
547
- if (!isPlainObject(t) || typeof t.name !== "string") return false;
548
- if (t.type === "namespace") return t.name === c.namePrefix;
549
- return t.name === c.namePrefix || t.name.startsWith(`${c.namePrefix}.`);
550
- }),
551
- ));
552
- if (conflicting.length === 0) return body;
553
-
554
- // Allocate a replacement only when a hosted tool is removed. Preserve malformed entries,
555
- // unrelated tools, and future tool types so this compatibility rule remains narrowly scoped.
556
- const stripGroup = (tools: unknown[]): unknown[] => {
557
- const filtered = tools.filter(t => {
558
- const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
559
- if (!type) return true;
560
- return !conflicting.some(c => c.hostedType === type);
561
- });
562
- return filtered.length === tools.length ? tools : filtered;
742
+
743
+ return groupChanged ? normalized : tools;
563
744
  };
564
745
 
565
746
  let changed = false;
566
747
  let tools = body.tools;
567
748
  if (Array.isArray(body.tools)) {
568
- tools = stripGroup(body.tools);
749
+ tools = normalizeGroup(body.tools);
569
750
  changed ||= tools !== body.tools;
570
751
  }
571
752
 
@@ -573,13 +754,15 @@ function stripConflictingHostedTools(body: unknown): unknown {
573
754
  if (Array.isArray(body.input)) {
574
755
  let nestedChanged = false;
575
756
  const mappedInput = body.input.map(item => {
576
- if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
577
- return item;
757
+ if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
758
+ const nestedTools = normalizeGroup(item.tools);
759
+ if (nestedTools === item.tools) return item;
760
+ nestedChanged = true;
761
+ return { ...item, tools: nestedTools };
578
762
  }
579
- const nestedTools = stripGroup(item.tools);
580
- if (nestedTools === item.tools) return item;
581
- nestedChanged = true;
582
- return { ...item, tools: nestedTools };
763
+ const normalizedCall = normalizeImageGenFunctionCall(item);
764
+ if (normalizedCall !== item) nestedChanged = true;
765
+ return normalizedCall;
583
766
  });
584
767
  if (nestedChanged) {
585
768
  input = mappedInput;
@@ -587,11 +770,15 @@ function stripConflictingHostedTools(body: unknown): unknown {
587
770
  }
588
771
  }
589
772
 
773
+ const toolChoice = normalizeImageGenToolChoice(body.tool_choice, toolChoiceAliases);
774
+ changed ||= toolChoice !== body.tool_choice;
775
+
590
776
  if (!changed) return body;
591
777
  return {
592
778
  ...body,
593
779
  ...(Array.isArray(body.tools) ? { tools } : {}),
594
780
  ...(Array.isArray(body.input) ? { input } : {}),
781
+ ...(Object.prototype.hasOwnProperty.call(body, "tool_choice") ? { tool_choice: toolChoice } : {}),
595
782
  };
596
783
  }
597
784
 
@@ -740,7 +927,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
740
927
  outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
741
928
  outBody = stripUnsupportedForwardParams(outBody);
742
929
  }
743
- else outBody = stripConflictingHostedTools(outBody);
930
+ else outBody = normalizeImageGenClientTools(outBody);
744
931
  if (forward || parsed._previousResponseInputExpanded === true) {
745
932
  outBody = repairOversizedReplayCallIds(outBody);
746
933
  }
@@ -27,6 +27,7 @@ export interface ClaudeAgentDef {
27
27
  name: string;
28
28
  model: string;
29
29
  description: string;
30
+ effort?: NonNullable<OcxConfig["claudeCode"]>["subagentEffort"];
30
31
  blockedSkills: readonly string[];
31
32
  }
32
33
 
@@ -99,6 +100,7 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
99
100
  name: `${OWNED_PREFIX}${unique}`,
100
101
  model,
101
102
  description,
103
+ effort: config.claudeCode?.subagentEffort,
102
104
  blockedSkills: blockedSkillsFor(model),
103
105
  });
104
106
  };
@@ -124,6 +126,7 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
124
126
  name: `${OWNED_PREFIX}self`,
125
127
  model: marked,
126
128
  description: `Self-clone: delegate to your default main model (${marked}), synced from the /model picker at launch. ${NO_MODEL_ARG}`,
129
+ effort: config.claudeCode?.subagentEffort,
127
130
  blockedSkills: blockedSkillsFor(marked),
128
131
  });
129
132
  }
@@ -149,6 +152,7 @@ function renderAgentDef(def: ClaudeAgentDef): string {
149
152
  `name: ${JSON.stringify(def.name)}`,
150
153
  `description: ${JSON.stringify(def.description)}`,
151
154
  `model: ${JSON.stringify(def.model)}`,
155
+ ...(def.effort ? [`effort: ${JSON.stringify(def.effort)}`] : []),
152
156
  "---",
153
157
  "",
154
158
  `<!-- ${GENERATED_MARKER} -->`,
@@ -157,6 +161,7 @@ function renderAgentDef(def: ClaudeAgentDef): string {
157
161
  // BODY rides the subagent's system prompt verbatim. The proxy detects this
158
162
  // directive and overrides the request model before routing/passthrough.
159
163
  `<!-- ocx-route: ${def.model} -->`,
164
+ ...(def.effort ? [`<!-- ocx-effort: ${def.effort} -->`] : []),
160
165
  "",
161
166
  `You are a delegated worker running on \`${def.model}\` through the local opencodex proxy.`,
162
167
  `IDENTITY: your ACTUAL underlying model is \`${def.model}\` — the opencodex proxy routes this`,
@@ -147,23 +147,40 @@ export function effectiveBlockedSkillNames(cc?: Pick<OcxClaudeCodeConfig, "block
147
147
  * FIRST directive wins; the scan is bounded to the system field.
148
148
  */
149
149
  const OCX_ROUTE_RE = /<!--\s*ocx-route:\s*([^\s]+)\s*-->/;
150
+ const OCX_EFFORT_RE = /<!--\s*ocx-effort:\s*(low|medium|high|xhigh|max)\s*-->/;
150
151
 
151
- export function extractOcxRouteDirective(body: unknown): string | null {
152
+ function systemText(body: unknown): string | null {
152
153
  if (!isRec(body)) return null;
153
154
  const system = body.system;
154
- let text: string | undefined;
155
- if (typeof system === "string") text = system;
156
- else if (Array.isArray(system)) {
157
- text = system
158
- .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
159
- .map(b => b.text as string)
160
- .join("\n");
161
- }
155
+ if (typeof system === "string") return system || null;
156
+ if (!Array.isArray(system)) return null;
157
+ const text = system
158
+ .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
159
+ .map(b => b.text as string)
160
+ .join("\n");
161
+ return text || null;
162
+ }
163
+
164
+ export function extractOcxRouteDirective(body: unknown): string | null {
165
+ const text = systemText(body);
162
166
  if (!text) return null;
163
167
  const match = OCX_ROUTE_RE.exec(text);
164
168
  return match ? match[1]! : null;
165
169
  }
166
170
 
171
+ /**
172
+ * Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and
173
+ * `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the
174
+ * exact generated-agent setting through the same trusted system-body channel as
175
+ * ocx-route so the inbound translator can restore `output_config.effort`.
176
+ */
177
+ export function extractOcxEffortDirective(body: unknown): NonNullable<OcxClaudeCodeConfig["subagentEffort"]> | null {
178
+ const text = systemText(body);
179
+ if (!text) return null;
180
+ const match = OCX_EFFORT_RE.exec(text);
181
+ return match ? match[1] as NonNullable<OcxClaudeCodeConfig["subagentEffort"]> : null;
182
+ }
183
+
167
184
  /** Injected-skill payloads below this size are never stubbed (not worth it). */
168
185
  const SKILL_ELISION_MIN_CHARS = 10_000;
169
186
  const SKILL_TEXT_MARKER = "Base directory for this skill: ";
@@ -137,11 +137,11 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
137
137
  for (let attempt = 0; attempt < 100; attempt++) {
138
138
  await Bun.sleep(2_000);
139
139
  const state = await runtimeRequest<Record<string, unknown>>(`/api/oauth/status?provider=${encodeURIComponent(provider)}`, {}, deps);
140
+ if (state.error) throw new CliUsageError(String(state.error));
140
141
  if (state.loggedIn === true) {
141
142
  printData(state, wantsJson, [`Logged in to ${provider}.`]);
142
143
  return;
143
144
  }
144
- if (state.error) throw new CliUsageError(String(state.error));
145
145
  }
146
146
  throw new CliUsageError("login timed out");
147
147
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Detects whether this process was started by a coding agent rather than typed
3
+ * by a person.
4
+ *
5
+ * Agent harnesses run `ocx` on the user's behalf and answer prompts from their
6
+ * own logic, which means a consent question would be decided by the agent
7
+ * instead of the account owner. Prompts that act on the user's identity check
8
+ * this and defer instead: they stay silent so the question reaches the human on
9
+ * a later hand-typed run.
10
+ *
11
+ * Detection is env-var based and deliberately conservative — a false positive
12
+ * only postpones a prompt, while a false negative would let an agent answer for
13
+ * the user.
14
+ */
15
+
16
+ /** Env vars set by agent harnesses and CI runners inside the shell they spawn. */
17
+ const AGENT_ENV_VARS = [
18
+ "CLAUDECODE",
19
+ "CLAUDE_CODE_ENTRYPOINT",
20
+ "CODEX_THREAD_ID",
21
+ "CODEX_SHELL",
22
+ "CODEX_CI",
23
+ "CURSOR_TRACE_ID",
24
+ "CURSOR_SESSION_TOKEN",
25
+ "AIDER_CHAT",
26
+ "REPL_ID",
27
+ "CI",
28
+ "GITHUB_ACTIONS",
29
+ ] as const;
30
+
31
+ /**
32
+ * True when an agent or automated runner is driving this process. Reads the
33
+ * environment on every call so tests and long-lived processes see current state.
34
+ */
35
+ export function isAgentDriven(env: NodeJS.ProcessEnv = process.env): boolean {
36
+ return AGENT_ENV_VARS.some(name => (env[name] ?? "").trim() !== "");
37
+ }
package/src/cli/claude.ts CHANGED
@@ -14,6 +14,7 @@ import { refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
14
14
  import { commandInvocation } from "../lib/win-exec";
15
15
  import { findLiveProxy } from "../server/proxy-liveness";
16
16
  import type { OcxConfig } from "../types";
17
+ import { configuredAdminToken } from "../lib/admin-secrets";
17
18
  import { PROXY_MARKER, ownAdmissionTokens, defaultAuthDetectDeps, detectClaudeAuth, type AuthDetectDeps } from "../claude/auth-detect";
18
19
  import { resolveClaudeAuthMode } from "../claude/auth-mode";
19
20
 
@@ -149,14 +150,13 @@ export function buildClaudeEnv(
149
150
 
150
151
  /**
151
152
  * Context-window map from the RUNNING proxy's management API (warm TTL cache; the
152
- * daemon registers every selector form — audit R3#1). 3s bound + auth header
153
- * (OPENCODEX_API_AUTH_TOKEN first, config key fallback — audit R4#1). Failure → {}
153
+ * daemon registers every selector form — audit R3#1). 3s bound + management auth header.
154
154
  * (no [1m] marking, conservative).
155
155
  */
156
156
  export async function fetchClaudeContextWindows(config: OcxConfig, port: number, timeoutMs = 3_000): Promise<Record<string, number>> {
157
157
  try {
158
158
  const headers = new Headers();
159
- const token = process.env.OPENCODEX_API_AUTH_TOKEN || config.apiKeys?.[0]?.key;
159
+ const token = configuredAdminToken();
160
160
  if (token) headers.set("x-opencodex-api-key", token);
161
161
  const res = await fetch(`http://127.0.0.1:${port}/api/claude-code`, {
162
162
  headers,