@bitkyc08/opencodex 2.11.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
|
@@ -113,7 +113,12 @@ function fallbackChainKey(model: string, namespaces: unknown): string {
|
|
|
113
113
|
return JSON.stringify(["account", selector, model.slice(slash + 1).toLowerCase()]);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function normalizedChain(
|
|
116
|
+
function normalizedChain(
|
|
117
|
+
primary: string,
|
|
118
|
+
config: OcxConfig,
|
|
119
|
+
extra: readonly string[] = [],
|
|
120
|
+
trailing: readonly string[] = [],
|
|
121
|
+
): string[] {
|
|
117
122
|
const chain: string[] = [];
|
|
118
123
|
const seen = new Set<string>();
|
|
119
124
|
const push = (model: string | undefined) => {
|
|
@@ -127,6 +132,7 @@ function normalizedChain(primary: string, config: OcxConfig, extra: readonly str
|
|
|
127
132
|
push(primary);
|
|
128
133
|
for (const model of extra) push(model);
|
|
129
134
|
for (const model of config.subagentModelFallback ?? []) push(model);
|
|
135
|
+
for (const model of trailing) push(model);
|
|
130
136
|
return chain;
|
|
131
137
|
}
|
|
132
138
|
|
|
@@ -268,8 +274,9 @@ export function selectAvailableSubagentModel(
|
|
|
268
274
|
now = Date.now(),
|
|
269
275
|
nativeFallbackOnly = false,
|
|
270
276
|
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
277
|
+
trailingFallback: readonly string[] = [],
|
|
271
278
|
): { model: string; rewritten: boolean; skipped: string[] } {
|
|
272
|
-
const chain = normalizedChain(primary, config, extraFallback);
|
|
279
|
+
const chain = normalizedChain(primary, config, extraFallback, trailingFallback);
|
|
273
280
|
const skipped: string[] = [];
|
|
274
281
|
for (const candidate of chain) {
|
|
275
282
|
if (nativeFallbackOnly) {
|
|
@@ -417,6 +424,37 @@ function subagentQuotaPrimeBlockedByHostCircuit(config: OcxConfig): boolean {
|
|
|
417
424
|
return getUpstreamHostHealth(key)?.cooldownUntil !== undefined;
|
|
418
425
|
}
|
|
419
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Per-primary-model fallback chains from opencodex config (#1190).
|
|
429
|
+
*
|
|
430
|
+
* Storing `model_fallback` inside `$CODEX_HOME/agents/*.toml` makes Codex >= 0.146
|
|
431
|
+
* reject the whole role file as an unknown field. The config-keyed map is the
|
|
432
|
+
* supported home for that metadata; keys match the requested primary model id,
|
|
433
|
+
* using the same raw/encoded slug tolerance as the TOML role lookup.
|
|
434
|
+
*/
|
|
435
|
+
export function resolveConfiguredModelFallbackForPrimary(
|
|
436
|
+
primary: string,
|
|
437
|
+
config: OcxConfig,
|
|
438
|
+
): string[] {
|
|
439
|
+
const byModel = config.subagentModelFallbackByModel;
|
|
440
|
+
if (!byModel || typeof byModel !== "object") return [];
|
|
441
|
+
const entries: string[] = [];
|
|
442
|
+
const seen = new Set<string>();
|
|
443
|
+
const push = (model: string) => {
|
|
444
|
+
const trimmed = model.trim();
|
|
445
|
+
if (trimmed === "") return;
|
|
446
|
+
const key = fallbackChainKey(trimmed, config.codexAccountNamespaces);
|
|
447
|
+
if (seen.has(key)) return;
|
|
448
|
+
seen.add(key);
|
|
449
|
+
entries.push(trimmed);
|
|
450
|
+
};
|
|
451
|
+
for (const [key, chain] of Object.entries(byModel)) {
|
|
452
|
+
if (!slugsEquivalent(key, primary)) continue;
|
|
453
|
+
for (const model of chain) push(model);
|
|
454
|
+
}
|
|
455
|
+
return entries;
|
|
456
|
+
}
|
|
457
|
+
|
|
420
458
|
/**
|
|
421
459
|
* Best-effort quota refresh before subagent model selection.
|
|
422
460
|
* Concurrent callers share one in-flight promise. The success TTL is updated only
|
|
@@ -479,21 +517,25 @@ export function applySubagentModelFallback(
|
|
|
479
517
|
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
480
518
|
): { from?: string; to?: string; skipped?: string[] } | null {
|
|
481
519
|
if (!isThreadSpawnRequest(headers)) return null;
|
|
482
|
-
const
|
|
520
|
+
const tomlRoleFallback = resolveAgentModelFallbackForPrimary(
|
|
483
521
|
parsed.modelId,
|
|
484
522
|
getCodexHome(),
|
|
485
523
|
config.codexAccountNamespaces,
|
|
486
524
|
);
|
|
525
|
+
// Config-keyed chains are the supported per-role home (#1190); TOML `model_fallback`
|
|
526
|
+
// stays readable for backwards compatibility with homes written before Codex 0.146.
|
|
527
|
+
const configuredFallback = resolveConfiguredModelFallbackForPrimary(parsed.modelId, config);
|
|
487
528
|
const globalFallback = config.subagentModelFallback ?? [];
|
|
488
|
-
if (globalFallback.length === 0 &&
|
|
529
|
+
if (globalFallback.length === 0 && configuredFallback.length === 0 && tomlRoleFallback.length === 0) return null;
|
|
489
530
|
const selection = selectAvailableSubagentModel(
|
|
490
531
|
parsed.modelId,
|
|
491
532
|
config,
|
|
492
|
-
|
|
533
|
+
configuredFallback,
|
|
493
534
|
accountId,
|
|
494
535
|
now,
|
|
495
536
|
nativeFallbackOnly,
|
|
496
537
|
accountUsabilityOptions,
|
|
538
|
+
tomlRoleFallback,
|
|
497
539
|
);
|
|
498
540
|
if (!selection.rewritten) return selection.skipped.length > 0
|
|
499
541
|
? { from: parsed.modelId, to: parsed.modelId, skipped: selection.skipped }
|
|
@@ -510,33 +552,192 @@ export function subagentFallbackGuidanceText(config: OcxConfig): string {
|
|
|
510
552
|
return ` Subagent model fallback chain (priority order): ${quoted}. When the primary model is quota-exhausted, opencodex rewrites thread_spawn requests to the next available model automatically.`;
|
|
511
553
|
}
|
|
512
554
|
|
|
513
|
-
const
|
|
555
|
+
const TOML_MODEL_FALLBACK_KEY = /^\s*(?:model_fallback|"model_fallback"|'model_fallback')\s*=/;
|
|
514
556
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
557
|
+
type TomlModelFallbackField = { present: false; value: null } | { present: true; value: string[] | null };
|
|
558
|
+
|
|
559
|
+
type TomlScanState = {
|
|
560
|
+
inMultilineString: '"""' | "'''" | null;
|
|
561
|
+
arrayDepth: number;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/** Track TOML strings, comments, and array brackets on one line. */
|
|
565
|
+
function scanTomlLine(line: string, state: TomlScanState): void {
|
|
566
|
+
let i = 0;
|
|
567
|
+
while (i < line.length) {
|
|
568
|
+
const ch = line[i]!;
|
|
569
|
+
if (ch === "#") return;
|
|
570
|
+
if (ch === '"' || ch === "'") {
|
|
571
|
+
const delimiter = ch.repeat(3);
|
|
572
|
+
if (line.startsWith(delimiter, i)) {
|
|
573
|
+
const end = findTomlMultilineStringEnd(line, i + 3, ch);
|
|
574
|
+
if (end === -1) {
|
|
575
|
+
state.inMultilineString = delimiter as '"""' | "'''";
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
i = end + 3;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
i++;
|
|
582
|
+
while (i < line.length) {
|
|
583
|
+
if (ch === '"' && line[i] === "\\") {
|
|
584
|
+
i += 2;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (line[i] === ch) break;
|
|
588
|
+
i++;
|
|
589
|
+
}
|
|
590
|
+
i++;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (ch === "[") state.arrayDepth++;
|
|
594
|
+
else if (ch === "]") state.arrayDepth = Math.max(0, state.arrayDepth - 1);
|
|
595
|
+
i++;
|
|
596
|
+
}
|
|
518
597
|
}
|
|
519
598
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
599
|
+
/** Parse a TOML string starting at `start`; returns the decoded value and end offset. */
|
|
600
|
+
function parseTomlStringAt(text: string, start: number): { value: string; end: number } | null {
|
|
601
|
+
const quote = text[start]!;
|
|
602
|
+
const delimiter = quote.repeat(3);
|
|
603
|
+
if (text.startsWith(delimiter, start)) {
|
|
604
|
+
const end = findTomlMultilineStringEnd(text, start + 3, quote);
|
|
605
|
+
if (end === -1) return null;
|
|
606
|
+
let value = text.slice(start + 3, end).trim();
|
|
607
|
+
if (quote === '"') value = value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
608
|
+
return { value, end: end + 3 };
|
|
609
|
+
}
|
|
610
|
+
let i = start + 1;
|
|
611
|
+
let value = "";
|
|
612
|
+
while (i < text.length) {
|
|
613
|
+
const ch = text[i]!;
|
|
614
|
+
if (quote === '"' && ch === "\\") {
|
|
615
|
+
const next = text[i + 1];
|
|
616
|
+
if (next === '"' || next === "\\") {
|
|
617
|
+
value += next;
|
|
618
|
+
i += 2;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (ch === quote) return { value, end: i + 1 };
|
|
623
|
+
value += ch;
|
|
624
|
+
i++;
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function findTomlMultilineStringEnd(text: string, from: number, quote: string): number {
|
|
630
|
+
const delimiter = quote.repeat(3);
|
|
631
|
+
let index = text.indexOf(delimiter, from);
|
|
632
|
+
while (index !== -1) {
|
|
633
|
+
if (quote === "'") return index;
|
|
634
|
+
let backslashes = 0;
|
|
635
|
+
for (let j = index - 1; j >= 0 && text[j] === "\\"; j--) backslashes += 1;
|
|
636
|
+
if (backslashes % 2 === 0) return index;
|
|
637
|
+
index = text.indexOf(delimiter, index + 1);
|
|
638
|
+
}
|
|
639
|
+
return -1;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** After an array close, only horizontal whitespace, an inline comment, or the line end is valid. */
|
|
643
|
+
function isValidTomlArrayTail(text: string, from: number): boolean {
|
|
644
|
+
let i = from;
|
|
645
|
+
while (i < text.length && (text[i] === " " || text[i] === "\t")) i += 1;
|
|
646
|
+
if (i >= text.length || text[i] === "\n" || text[i] === "\r") return true;
|
|
647
|
+
if (text[i] === "#") {
|
|
648
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Parse a TOML string-array value; null when the value is not a well-formed array of strings. */
|
|
655
|
+
function parseTomlStringArrayValue(text: string): string[] | null {
|
|
656
|
+
const values: string[] = [];
|
|
657
|
+
let i = 0;
|
|
658
|
+
const skipIgnored = () => {
|
|
659
|
+
while (i < text.length) {
|
|
660
|
+
const ch = text[i]!;
|
|
661
|
+
if (ch === "#") {
|
|
662
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (/\s/.test(ch)) {
|
|
666
|
+
i += 1;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
skipIgnored();
|
|
673
|
+
if (text[i] !== "[") return null;
|
|
674
|
+
i += 1;
|
|
675
|
+
let expectValue = true;
|
|
676
|
+
for (;;) {
|
|
677
|
+
skipIgnored();
|
|
678
|
+
if (i >= text.length) return null;
|
|
679
|
+
const ch = text[i]!;
|
|
680
|
+
if (ch === "]") {
|
|
681
|
+
if (!isValidTomlArrayTail(text, i + 1)) return null;
|
|
682
|
+
return values;
|
|
683
|
+
}
|
|
684
|
+
if (ch === ",") {
|
|
685
|
+
if (expectValue) return null; // leading or doubled comma
|
|
686
|
+
expectValue = true;
|
|
687
|
+
i += 1;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (ch === '"' || ch === "'") {
|
|
691
|
+
if (!expectValue) return null; // adjacent strings must be comma-separated
|
|
692
|
+
const parsed = parseTomlStringAt(text, i);
|
|
693
|
+
if (!parsed) return null;
|
|
694
|
+
values.push(parsed.value);
|
|
695
|
+
i = parsed.end;
|
|
696
|
+
expectValue = false;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* TOML-aware, presence-aware parse of the `model_fallback` field. Quoted keys
|
|
705
|
+
* are recognized, and text inside strings (including multiline strings) is
|
|
706
|
+
* never treated as a key. `present` is true whenever the key exists, even when
|
|
707
|
+
* the value is not a readable string array; `value` is null in that case.
|
|
708
|
+
*/
|
|
709
|
+
function parseTomlModelFallbackField(content: string): TomlModelFallbackField {
|
|
710
|
+
const lines = content.split(/\r?\n/);
|
|
711
|
+
const state: TomlScanState = { inMultilineString: null, arrayDepth: 0 };
|
|
712
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
713
|
+
const line = lines[i]!;
|
|
714
|
+
if (state.inMultilineString) {
|
|
715
|
+
const end = findTomlMultilineStringEnd(line, 0, state.inMultilineString[0]!);
|
|
716
|
+
if (end === -1) continue;
|
|
717
|
+
state.inMultilineString = null;
|
|
718
|
+
scanTomlLine(line.slice(end + 3), state);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (state.arrayDepth === 0) {
|
|
722
|
+
const key = line.match(TOML_MODEL_FALLBACK_KEY);
|
|
723
|
+
if (key) {
|
|
724
|
+
const rest = `${line.slice(key[0].length)}\n${lines.slice(i + 1).join("\n")}`;
|
|
725
|
+
return { present: true, value: parseTomlStringArrayValue(rest) };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
scanTomlLine(line, state);
|
|
729
|
+
}
|
|
730
|
+
return { present: false, value: null };
|
|
524
731
|
}
|
|
525
732
|
|
|
526
733
|
export function readAgentModelFallback(filePath: string): string[] | null {
|
|
527
734
|
try {
|
|
528
735
|
const content = readFileSync(filePath, "utf8");
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
for (const line of content.split(/\r?\n/)) {
|
|
532
|
-
const match = line.match(TOML_STRING_ARRAY);
|
|
533
|
-
if (!match) continue;
|
|
534
|
-
return parseTomlStringArray(match[2] ?? "");
|
|
535
|
-
}
|
|
736
|
+
const parsed = parseTomlModelFallbackField(content);
|
|
737
|
+
return parsed.present ? parsed.value : null;
|
|
536
738
|
} catch {
|
|
537
739
|
return null;
|
|
538
740
|
}
|
|
539
|
-
return null;
|
|
540
741
|
}
|
|
541
742
|
|
|
542
743
|
export function readCodexAgentModelFallback(role: string, codexHome = CODEX_HOME): string[] {
|
|
@@ -545,6 +746,29 @@ export function readCodexAgentModelFallback(role: string, codexHome = CODEX_HOME
|
|
|
545
746
|
return readAgentModelFallback(file) ?? [];
|
|
546
747
|
}
|
|
547
748
|
|
|
749
|
+
/**
|
|
750
|
+
* True when the role TOML carries a `model_fallback` key, even an empty array.
|
|
751
|
+
* Presence is what matters for the doctor scan: Codex >= 0.146 rejects the
|
|
752
|
+
* field as unknown and skips the whole role regardless of its value. Uses the
|
|
753
|
+
* same TOML-aware parse as the fallback-reading path, so quoted keys count and
|
|
754
|
+
* text inside string literals does not.
|
|
755
|
+
*/
|
|
756
|
+
export function hasCodexAgentModelFallbackField(role: string, codexHome = CODEX_HOME): boolean {
|
|
757
|
+
const file = join(codexHome, "agents", `${role}.toml`);
|
|
758
|
+
if (!existsSync(file)) return false;
|
|
759
|
+
try {
|
|
760
|
+
const content = readFileSync(file, "utf8");
|
|
761
|
+
return parseTomlModelFallbackField(content).present;
|
|
762
|
+
} catch {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Roles whose TOML still carries `model_fallback`, including empty arrays. */
|
|
768
|
+
export function scanCodexAgentRolesWithTomlModelFallback(codexHome = CODEX_HOME): string[] {
|
|
769
|
+
return listCodexAgentRoles(codexHome).filter(role => hasCodexAgentModelFallbackField(role, codexHome));
|
|
770
|
+
}
|
|
771
|
+
|
|
548
772
|
export function listCodexAgentRoles(codexHome = CODEX_HOME): string[] {
|
|
549
773
|
const dir = join(codexHome, "agents");
|
|
550
774
|
if (!existsSync(dir)) return [];
|
|
@@ -557,4 +781,4 @@ export function listCodexAgentRoles(codexHome = CODEX_HOME): string[] {
|
|
|
557
781
|
export function shouldPrimeSubagentQuota(config: OcxConfig, now = Date.now()): boolean {
|
|
558
782
|
const last = quotaPrimedAt.get("global") ?? 0;
|
|
559
783
|
return now - last >= pollIntervalMs(config);
|
|
560
|
-
}
|
|
784
|
+
}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { isAbsolute, join, resolve } from "node:path";
|
|
19
19
|
|
|
20
|
+
import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
|
|
21
|
+
|
|
20
22
|
import type {
|
|
21
23
|
ResolveCodexCoordinatorDatabasePath,
|
|
22
24
|
ResolveCodexCatalogSerializationDatabasePath,
|
|
@@ -30,6 +32,13 @@ const POSIX_TMP_REQUIRED_MODE = 0o1003;
|
|
|
30
32
|
const POSIX_TMP_PATH = "/tmp";
|
|
31
33
|
const SID_PATTERN = /^S-1-(?:\d+-)+\d+$/i;
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Hard budget for the Windows identity-lookup PowerShell child. These lookups
|
|
37
|
+
* run at startup and on config writes; a hung child must fail the lookup
|
|
38
|
+
* (recoverable — the caller refuses) rather than wedge the proxy indefinitely.
|
|
39
|
+
*/
|
|
40
|
+
const WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS = 8_000;
|
|
41
|
+
|
|
33
42
|
export class CodexUserIdentityRefusal extends Error {
|
|
34
43
|
readonly code = "CODEX_USER_IDENTITY_REFUSED";
|
|
35
44
|
|
|
@@ -43,24 +52,66 @@ function refuse(message: string, cause?: unknown): never {
|
|
|
43
52
|
throw new CodexUserIdentityRefusal(message, cause === undefined ? undefined : { cause });
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
function windowsIdentityPowerShellCommand(expression: string): string[] {
|
|
56
|
+
return [
|
|
57
|
+
resolveTrustedWindowsPowerShellExe(),
|
|
58
|
+
"-NoLogo",
|
|
59
|
+
"-NoProfile",
|
|
60
|
+
"-NonInteractive",
|
|
61
|
+
"-WindowStyle",
|
|
62
|
+
"Hidden",
|
|
63
|
+
"-Command",
|
|
64
|
+
expression,
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function windowsIdentityPowerShellSpawnOptions(): {
|
|
69
|
+
stdin: "ignore";
|
|
70
|
+
stdout: "pipe";
|
|
71
|
+
stderr: "pipe";
|
|
72
|
+
timeout: number;
|
|
73
|
+
windowsHide: boolean;
|
|
74
|
+
} {
|
|
75
|
+
return {
|
|
76
|
+
stdin: "ignore",
|
|
77
|
+
stdout: "pipe",
|
|
78
|
+
stderr: "pipe",
|
|
79
|
+
timeout: WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS,
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Test-only readback of the trusted executable and static arguments (#1278). */
|
|
85
|
+
export function windowsIdentityPowerShellCommandForTests(expression: string): string[] {
|
|
86
|
+
return windowsIdentityPowerShellCommand(expression);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Test-only readback of the spawn options shared by the identity lookups (#1278). */
|
|
90
|
+
export function windowsIdentityPowerShellSpawnOptionsForTests(): ReturnType<
|
|
91
|
+
typeof windowsIdentityPowerShellSpawnOptions
|
|
92
|
+
> {
|
|
93
|
+
return windowsIdentityPowerShellSpawnOptions();
|
|
94
|
+
}
|
|
95
|
+
|
|
46
96
|
function powershellValue(expression: string): string {
|
|
97
|
+
let command: string[];
|
|
98
|
+
try {
|
|
99
|
+
command = windowsIdentityPowerShellCommand(expression);
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
refuse("Windows effective-account lookup could not start.", cause);
|
|
102
|
+
}
|
|
47
103
|
let result: ReturnType<typeof Bun.spawnSync>;
|
|
48
104
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expression,
|
|
56
|
-
], {
|
|
57
|
-
stdin: "ignore",
|
|
58
|
-
stdout: "pipe",
|
|
59
|
-
stderr: "pipe",
|
|
60
|
-
});
|
|
105
|
+
// `windowsHide` is the popup fix (#1278): the desktop proxy parent runs
|
|
106
|
+
// without a console, so a console-subsystem child spawned without
|
|
107
|
+
// CREATE_NO_WINDOW gets a fresh visible console window at startup and on
|
|
108
|
+
// every config write. `-WindowStyle Hidden` alone does not stop the
|
|
109
|
+
// allocation; the flag behind `windowsHide` does.
|
|
110
|
+
result = Bun.spawnSync(command, windowsIdentityPowerShellSpawnOptions());
|
|
61
111
|
} catch (cause) {
|
|
62
112
|
refuse("Windows effective-account lookup could not start.", cause);
|
|
63
113
|
}
|
|
114
|
+
if (result.exitedDueToTimeout) refuse("Windows effective-account lookup timed out.");
|
|
64
115
|
if (result.exitCode !== 0) refuse("Windows effective-account lookup failed.");
|
|
65
116
|
const value = new TextDecoder().decode(result.stdout).trim();
|
|
66
117
|
if (!value) refuse("Windows effective-account lookup returned an empty value.");
|