@bitkyc08/opencodex 2.12.0 → 2.13.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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,191 @@
1
+ import type { OcxConfig, OcxProviderConfig } from "../../types";
2
+ import type { InboundWire } from "../../providers/registry";
3
+ import { subjectIdForSubject } from "../../lab/digest";
4
+ import { buildRouteSubjectV1 } from "../../lab/subject/route-subject";
5
+ import { buildProtocolSubjectV1 } from "../../lab/subject/protocol-subject";
6
+ import { readExistingInstallationSalt } from "../../lab/subject/installation-salt";
7
+ import type { LabDestinationV1, LabRouteContext } from "../../lab/live/types";
8
+ import { resolveWireProtocolOverride } from "../../server/adapter-resolve";
9
+ import { endpointFingerprintFromBaseUrl, parseEndpointFingerprintParts } from "./endpoint";
10
+ import {
11
+ providerInstanceKey,
12
+ resolveProductionBehaviorValues,
13
+ surfaceForProtocols,
14
+ upstreamProtocolForAdapter,
15
+ } from "./behavior";
16
+ import { readOpenCodexCompatibilityVersion } from "./version";
17
+ import type { RoutingCompatibilityEvidenceLayer } from "./types";
18
+
19
+ const POLICY_INBOUND_WIRE: InboundWire = "responses";
20
+
21
+ export interface ResolvedPolicyRouteSubject {
22
+ subjectId: string;
23
+ routeContext: LabRouteContext;
24
+ destination: LabDestinationV1;
25
+ }
26
+
27
+ export interface ResolvedPolicyCompatibilitySubjects {
28
+ subjectIds: Partial<Record<RoutingCompatibilityEvidenceLayer, string>>;
29
+ route?: ResolvedPolicyRouteSubject;
30
+ }
31
+
32
+ /** Canonical Lab protocol identity for a production request wire. */
33
+ export function inboundProtocolForWire(inboundWire: InboundWire): string {
34
+ switch (inboundWire) {
35
+ case "responses": return "openai-responses";
36
+ case "chat": return "openai-chat";
37
+ case "anthropic": return "anthropic-messages";
38
+ }
39
+ }
40
+
41
+ function destinationSnapshotFromBaseUrl(baseUrl: string, fingerprint: string): LabDestinationV1 {
42
+ const parts = parseEndpointFingerprintParts(baseUrl);
43
+ if (!parts) throw new Error("invalid provider baseUrl for route subject");
44
+ return Object.freeze({
45
+ scheme: parts.scheme,
46
+ host: parts.host,
47
+ port: parts.port,
48
+ basePath: parts.basePath,
49
+ sniHost: parts.host,
50
+ addresses: Object.freeze([]),
51
+ privateNetwork: false,
52
+ fingerprint,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Resolve subject identities for one exact inbound wire without network I/O,
58
+ * projection reads/rebuilds, ledger replay, or Lab-state creation.
59
+ */
60
+ export function resolveCompatibilitySubjectsForInboundWire(
61
+ config: OcxConfig,
62
+ providerName: string,
63
+ modelId: string,
64
+ routed: OcxProviderConfig,
65
+ inboundWire: InboundWire,
66
+ configDir?: string,
67
+ ): ResolvedPolicyCompatibilitySubjects {
68
+ const effective = resolveWireProtocolOverride(providerName, modelId, routed, inboundWire);
69
+ const baseUrl = typeof effective.baseUrl === "string" ? effective.baseUrl.trim() : "";
70
+ const adapter = effective.adapter ?? "openai-responses";
71
+ const inboundProtocol = inboundProtocolForWire(inboundWire);
72
+ const upstreamProtocol = upstreamProtocolForAdapter(adapter);
73
+ const surface = surfaceForProtocols(inboundProtocol, upstreamProtocol);
74
+ const subjectIds: ResolvedPolicyCompatibilitySubjects["subjectIds"] = {};
75
+
76
+ try {
77
+ const protocolSubject = buildProtocolSubjectV1({
78
+ inboundProtocol,
79
+ upstreamProtocol,
80
+ surface,
81
+ }, adapter);
82
+ subjectIds.protocol_conformance = subjectIdForSubject(protocolSubject);
83
+ } catch {
84
+ // No CL-01 protocol subject exists for this exact adapter/wire identity.
85
+ }
86
+
87
+ if (!baseUrl) return { subjectIds };
88
+
89
+ let installationSalt: Uint8Array | null;
90
+ try {
91
+ installationSalt = readExistingInstallationSalt(configDir);
92
+ } catch {
93
+ installationSalt = null;
94
+ }
95
+ const compatibilityVersion = readOpenCodexCompatibilityVersion();
96
+ if (!installationSalt || !compatibilityVersion) return { subjectIds };
97
+
98
+ const endpointFp = endpointFingerprintFromBaseUrl(baseUrl, installationSalt);
99
+ if (!endpointFp) return { subjectIds };
100
+ const destination = destinationSnapshotFromBaseUrl(baseUrl, endpointFp);
101
+ const behaviorValues = resolveProductionBehaviorValues(
102
+ config,
103
+ providerName,
104
+ modelId,
105
+ effective,
106
+ installationSalt,
107
+ );
108
+ if (!behaviorValues) return { subjectIds };
109
+
110
+ const routeContext: LabRouteContext = {
111
+ providerId: providerName,
112
+ providerInstanceKey: providerInstanceKey(providerName, effective),
113
+ clientModelId: modelId,
114
+ upstreamModelId: modelId,
115
+ effectiveAdapter: adapter,
116
+ inboundProtocol,
117
+ upstreamProtocol,
118
+ surface,
119
+ baseUrl,
120
+ opencodexCompatibilityVersion: compatibilityVersion,
121
+ behaviorValues,
122
+ dependencies: [],
123
+ };
124
+
125
+ try {
126
+ const subject = buildRouteSubjectV1(routeContext, destination, configDir, installationSalt);
127
+ const route: ResolvedPolicyRouteSubject = {
128
+ subjectId: subjectIdForSubject(subject),
129
+ routeContext,
130
+ destination,
131
+ };
132
+ subjectIds.live_route_compatibility = route.subjectId;
133
+ return { subjectIds, route };
134
+ } catch {
135
+ return { subjectIds };
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Resolve the subject identities a policy candidate may legitimately consume.
141
+ * CL-06 policy evaluation is a Responses-surface lookup and remains unchanged.
142
+ */
143
+ export function resolvePolicyCompatibilitySubjects(
144
+ config: OcxConfig,
145
+ providerName: string,
146
+ modelId: string,
147
+ routed: OcxProviderConfig,
148
+ configDir?: string,
149
+ ): ResolvedPolicyCompatibilitySubjects {
150
+ return resolveCompatibilitySubjectsForInboundWire(
151
+ config,
152
+ providerName,
153
+ modelId,
154
+ routed,
155
+ POLICY_INBOUND_WIRE,
156
+ configDir,
157
+ );
158
+ }
159
+
160
+ /** Build exact RouteSubjectV1 identity for a policy candidate without network I/O. */
161
+ export function resolvePolicyRouteSubject(
162
+ config: OcxConfig,
163
+ providerName: string,
164
+ modelId: string,
165
+ routed: OcxProviderConfig,
166
+ configDir?: string,
167
+ ): ResolvedPolicyRouteSubject | null {
168
+ return resolvePolicyCompatibilitySubjects(config, providerName, modelId, routed, configDir).route ?? null;
169
+ }
170
+
171
+ /**
172
+ * Build the exact production-attempt RouteSubjectV1 identity for its actual
173
+ * inbound wire. Returns null when no existing Lab salt/identity can be read.
174
+ */
175
+ export function resolveProductionRouteSubject(
176
+ config: OcxConfig,
177
+ providerName: string,
178
+ modelId: string,
179
+ routed: OcxProviderConfig,
180
+ inboundWire: InboundWire,
181
+ configDir?: string,
182
+ ): ResolvedPolicyRouteSubject | null {
183
+ return resolveCompatibilitySubjectsForInboundWire(
184
+ config,
185
+ providerName,
186
+ modelId,
187
+ routed,
188
+ inboundWire,
189
+ configDir,
190
+ ).route ?? null;
191
+ }
@@ -0,0 +1,64 @@
1
+ import type { CompatibilityVerdict, EvidenceLayer } from "../../lab/constants";
2
+ import type { OcxRoutingUnknownEvidenceMode } from "../../types";
3
+
4
+ export type RoutingCompatibilityEvidenceLayer = Extract<
5
+ EvidenceLayer,
6
+ "protocol_conformance" | "live_route_compatibility"
7
+ >;
8
+
9
+ export interface CompatibilitySuiteRequirement {
10
+ suiteId: string;
11
+ evidenceLayer: RoutingCompatibilityEvidenceLayer;
12
+ }
13
+
14
+ export interface NormalizedCompatibilityPolicy {
15
+ requiredSuites: CompatibilitySuiteRequirement[];
16
+ minStatus?: "PROBED" | "VERIFIED";
17
+ maxEvidenceAgeMs?: number;
18
+ unknownEvidence: OcxRoutingUnknownEvidenceMode;
19
+ degradedEvidence: OcxRoutingUnknownEvidenceMode;
20
+ }
21
+
22
+ export interface ObservedSuiteVerdict {
23
+ subjectId: string;
24
+ suiteId: string;
25
+ evidenceLayer: RoutingCompatibilityEvidenceLayer;
26
+ suiteVersion: string;
27
+ suiteManifestDigest: string;
28
+ verdict: CompatibilityVerdict;
29
+ asOf: number;
30
+ /** Current catalog freshness ceiling for this exact suite identity. */
31
+ maxAgeMs: number | null;
32
+ notes: string[];
33
+ }
34
+
35
+ export interface CandidateCompatibilityEvidence {
36
+ subjectIds: Partial<Record<RoutingCompatibilityEvidenceLayer, string>>;
37
+ projectionAvailable: boolean;
38
+ suites: ObservedSuiteVerdict[];
39
+ }
40
+
41
+ export interface CompatibilityEvaluationOutcome {
42
+ exclusions: Array<{ code: string; detail?: string }>;
43
+ /** Numeric floor applied only when an explicit penalize policy fires. */
44
+ penaltyScore: number | null;
45
+ suiteTraces: Array<{
46
+ suiteId: string;
47
+ evidenceLayer: RoutingCompatibilityEvidenceLayer;
48
+ subjectIdPrefix?: string;
49
+ verdict?: CompatibilityVerdict;
50
+ minStatus?: "PROBED" | "VERIFIED";
51
+ fresh?: boolean;
52
+ unknownPolicy?: OcxRoutingUnknownEvidenceMode;
53
+ degradedPolicy?: OcxRoutingUnknownEvidenceMode;
54
+ outcome: "satisfied" | "penalized" | "excluded" | "unknown";
55
+ reason?: string;
56
+ }>;
57
+ traceTruncated: boolean;
58
+ }
59
+
60
+ export const COMPATIBILITY_UNKNOWN_PENALTY_SCORE = 0.3;
61
+ /** Compatibility is a bounded penalty dimension, not a universal score. */
62
+ export const COMPATIBILITY_WEIGHT = 0.05;
63
+ export const MAX_TRACE_COMPATIBILITY_SUITES = 8;
64
+ export const MAX_COMPATIBILITY_REQUIRED_SUITES = MAX_TRACE_COMPATIBILITY_SUITES;
@@ -0,0 +1,104 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { jcsStringify } from "../../lab/digest";
5
+
6
+ const VERSION_DOMAIN = "ocx-lab:compatibility-version:v1";
7
+ let cachedVersion: string | null | undefined;
8
+ let testOverride: string | null | undefined;
9
+
10
+ interface CompatibilityVersionManifest {
11
+ schemaVersion: 1;
12
+ assertionDslVersion: string;
13
+ evidenceSchemaVersion: string;
14
+ bunRuntimeVersion: string;
15
+ files: Array<{ path: string; sha256: string }>;
16
+ }
17
+
18
+ function isSha256Hex(value: string): boolean {
19
+ return /^[0-9a-f]{64}$/.test(value);
20
+ }
21
+
22
+ function hashManifest(manifest: unknown): string {
23
+ const hash = createHash("sha256");
24
+ hash.update(new TextEncoder().encode(`${VERSION_DOMAIN}\0`));
25
+ hash.update(new TextEncoder().encode(jcsStringify(manifest)));
26
+ return hash.digest("hex");
27
+ }
28
+
29
+ function parseEmbeddedManifest(raw: unknown): CompatibilityVersionManifest | null {
30
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
31
+ const record = raw as Record<string, unknown>;
32
+ if (record.schemaVersion !== 1
33
+ || typeof record.assertionDslVersion !== "string"
34
+ || typeof record.evidenceSchemaVersion !== "string"
35
+ || typeof record.bunRuntimeVersion !== "string"
36
+ || !Array.isArray(record.files)
37
+ || record.files.length === 0) return null;
38
+ const files: CompatibilityVersionManifest["files"] = [];
39
+ const seen = new Set<string>();
40
+ for (const row of record.files) {
41
+ if (!row || typeof row !== "object" || Array.isArray(row)) return null;
42
+ const path = (row as Record<string, unknown>).path;
43
+ const sha256 = (row as Record<string, unknown>).sha256;
44
+ if (typeof path !== "string" || !path || path.includes("\\") || path.startsWith("/") || path.split("/").includes("..")) return null;
45
+ if (typeof sha256 !== "string" || !isSha256Hex(sha256) || seen.has(path)) return null;
46
+ seen.add(path);
47
+ files.push({ path, sha256 });
48
+ }
49
+ return {
50
+ schemaVersion: 1,
51
+ assertionDslVersion: record.assertionDslVersion,
52
+ evidenceSchemaVersion: record.evidenceSchemaVersion,
53
+ // The runtime version is part of the canonical identity. The embedded file
54
+ // hashes remain valid across bundled Bun updates, while this field reflects
55
+ // the actual executing runtime rather than the package-build runtime.
56
+ bunRuntimeVersion: Bun.version,
57
+ files,
58
+ };
59
+ }
60
+
61
+ function readEmbeddedManifest(): CompatibilityVersionManifest | null {
62
+ const candidates = [
63
+ join(import.meta.dir, "..", "..", "generated", "compatibility-version.json"),
64
+ join(import.meta.dir, "..", "..", "..", "src", "generated", "compatibility-version.json"),
65
+ ];
66
+ for (const path of candidates) {
67
+ try {
68
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
69
+ const manifest = parseEmbeddedManifest(parsed);
70
+ if (manifest) return manifest;
71
+ } catch {
72
+ // try next candidate
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * OpenCodex compatibility version for route subject identity.
80
+ *
81
+ * There is intentionally no runtime-only fallback: without the generated file
82
+ * manifest we cannot prove exact implementation identity, so live-route subject
83
+ * resolution fails closed and follows the profile's unknown-evidence policy.
84
+ */
85
+ export function readOpenCodexCompatibilityVersion(): string | null {
86
+ if (testOverride !== undefined) return testOverride;
87
+ if (cachedVersion !== undefined) return cachedVersion;
88
+ const embedded = readEmbeddedManifest();
89
+ cachedVersion = embedded ? hashManifest(embedded) : null;
90
+ return cachedVersion;
91
+ }
92
+
93
+ /** Test-only exact identity override; never sourced from process environment. */
94
+ export function setCompatibilityVersionOverrideForTests(value: string | null): void {
95
+ if (value !== null && !isSha256Hex(value)) throw new Error("invalid test compatibility version");
96
+ testOverride = value;
97
+ cachedVersion = undefined;
98
+ }
99
+
100
+ /** Test-only reset of the compatibility version memo and override. */
101
+ export function resetCompatibilityVersionCacheForTests(): void {
102
+ cachedVersion = undefined;
103
+ testOverride = undefined;
104
+ }
@@ -24,6 +24,9 @@ import { getRoutingProfile, policyModelId, type NormalizedRoutingProfile } from
24
24
  import { healthScore } from "./health";
25
25
  import { quotaScore } from "./quota";
26
26
  import { costScore } from "./cost";
27
+ import { evaluateCompatibilityForCandidate } from "./compatibility/policy";
28
+ import { COMPATIBILITY_WEIGHT, type CandidateCompatibilityEvidence } from "./compatibility/types";
29
+ import type { RouteCompatibilityEvidence } from "./trace";
27
30
 
28
31
  /** Unknown health under "penalize": a low-but-not-zero deterministic floor. */
29
32
  export const HEALTH_UNKNOWN_PENALTY_SCORE = 0.3;
@@ -55,6 +58,7 @@ export interface PolicyCandidateEvidence {
55
58
  health?: RouteHealthEvidence;
56
59
  quota?: RouteQuotaEvidence;
57
60
  cost?: RouteCostEvidence;
61
+ compatibility?: CandidateCompatibilityEvidence;
58
62
  }
59
63
 
60
64
  export interface PolicyEvaluationCandidate {
@@ -68,6 +72,7 @@ export interface PolicyEvaluationCandidate {
68
72
  health?: RouteHealthEvidence;
69
73
  quota?: RouteQuotaEvidence;
70
74
  cost?: RouteCostEvidence;
75
+ compatibility?: RouteCompatibilityEvidence;
71
76
  score?: RouteScoreEvidence;
72
77
  }
73
78
 
@@ -85,8 +90,6 @@ function booleanRequirement(
85
90
  actual: Unknownable | undefined,
86
91
  ): RouteRequirementEvidence | null {
87
92
  if (required === undefined) return null;
88
- // Note: `required === false` is a negative assertion ("must NOT have X"),
89
- // not "don't care"; an absent field is the only "no requirement" form.
90
93
  if (actual === undefined || actual === "unknown") {
91
94
  return { id, expected: required, outcome: "unknown" };
92
95
  }
@@ -117,11 +120,6 @@ function requirementFor(
117
120
  requirements.push({ id: "min-context-window", expected: require.minContextWindow, outcome: "unknown" });
118
121
  }
119
122
  }
120
- // minQuotaHeadroom gates only KNOWN quota via the normalized score, which
121
- // maps exhaustion to zero and unknown/incomplete evidence to null. Unknown
122
- // quota is governed by the profile's `unknownEvidence.quota` policy
123
- // (exclude / penalize / allow) via the quota score path - never by the
124
- // capability unknown policy.
125
123
  if (require.minQuotaHeadroom !== undefined) {
126
124
  const quotaHeadroom = quotaScore(quota);
127
125
  if (quotaHeadroom !== null) {
@@ -186,12 +184,6 @@ function configuredPriorityScore(index: number, total: number): number {
186
184
  return total > 1 ? (total - index) / total : 1;
187
185
  }
188
186
 
189
- /**
190
- * Requirements derived from the request evidence supplied to a dry-run. A
191
- * candidate must satisfy both the profile `require` block and the request
192
- * needs (context window, tools, image input, structured output, reasoning
193
- * effort, service tier, encrypted Codex tasks) to be eligible.
194
- */
195
187
  function requestRequirementFor(
196
188
  request: PolicyRequestEvidence,
197
189
  capability: RouteCapabilityEvidence | undefined,
@@ -210,7 +202,6 @@ function requestRequirementFor(
210
202
  requirements.push({ id: "request-context-window", expected: request.contextWindow, outcome: "unknown" });
211
203
  }
212
204
  }
213
- // Absent flags mean "no requirement": only a positive request need adds a row.
214
205
  if (request.toolsRequired === true) {
215
206
  const tools = booleanRequirement("request-tools", true, capability?.tools);
216
207
  if (tools) requirements.push(tools);
@@ -297,10 +288,6 @@ export function evaluatePolicyProfile(
297
288
  }
298
289
  const unsatisfied = bad.some(requirement => requirement.outcome === "unsatisfied");
299
290
  const unknown = bad.some(requirement => requirement.outcome === "unknown");
300
- // Unknown capability handling per profile: exclude (default), penalize,
301
- // or allow. "penalize" cannot move the score while configuredPriorityScore
302
- // is the only component; the capability score dimension is still future
303
- // (RI-06+), so "penalize" behaves identically to "allow" in this release.
304
291
  const excludedByUnknown = unknown && profile.unknownEvidence.capability === "exclude";
305
292
  const costLimit = profile.limits.maxEstimatedCostUsd;
306
293
  const estimatedCost = evidence.cost?.estimatedUsd;
@@ -357,9 +344,18 @@ export function evaluatePolicyProfile(
357
344
  }
358
345
  }
359
346
 
360
- // Health scoring (RI-06): live hard cooldown is authoritative and
361
- // excludes; unknown health follows the profile's unknownEvidence policy;
362
- // historical health never overrides explicit ineligibility.
347
+ const compatibilityEval = evaluateCompatibilityForCandidate(
348
+ profile.compatibility,
349
+ evidence.compatibility,
350
+ now,
351
+ );
352
+ if (compatibilityEval.exclusions.length > 0) {
353
+ exclusions.push(...compatibilityEval.exclusions);
354
+ eligible = false;
355
+ }
356
+ const compatibilityValue = compatibilityEval.penaltyScore;
357
+ const compatibilityTrace = compatibilityEval.trace;
358
+
363
359
  const health = evidence.health;
364
360
  let healthValue = health ? healthScore(health, now) : null;
365
361
  if (health?.cooldownUntilMs !== undefined && health.cooldownUntilMs > now) {
@@ -371,13 +367,9 @@ export function evaluatePolicyProfile(
371
367
  } else if (healthValue === null && profile.unknownEvidence.health === "penalize") {
372
368
  healthValue = HEALTH_UNKNOWN_PENALTY_SCORE;
373
369
  } else if (healthValue === null && profile.unknownEvidence.health === "allow") {
374
- // Neutral midpoint: blending keeps an unknown candidate from outranking
375
- // a measured one with the same configured priority.
376
370
  healthValue = HEALTH_UNKNOWN_NEUTRAL_SCORE;
377
371
  }
378
372
 
379
- // Quota scoring (RI-07): unknown quota follows the profile policy;
380
- // exhausted or low-headroom evidence lowers the score.
381
373
  const quota = evidence.quota;
382
374
  let quotaValue = quota ? quotaScore(quota) : null;
383
375
  if (quotaValue === null) {
@@ -406,11 +398,6 @@ export function evaluatePolicyProfile(
406
398
  const healthWeight = profile.optimize.health;
407
399
  const quotaWeight = profile.optimize.quota;
408
400
  const costWeight = profile.optimize.cost;
409
- // Only spend a dimension's weight when a value is actually present:
410
- // "allow" leaves missing health/quota/cost components null, so subtracting
411
- // their weights would shrink the priority share for evidence the profile
412
- // explicitly permits to be absent. Renormalize those weights back into
413
- // priority instead of silently changing the ranking semantics.
414
401
  const spentHealth = healthValue !== null ? healthWeight : 0;
415
402
  const spentQuota = quotaValue !== null ? quotaWeight : 0;
416
403
  const spentCost = costValue !== null ? costWeight : 0;
@@ -429,6 +416,13 @@ export function evaluatePolicyProfile(
429
416
  total += costWeight * costValue;
430
417
  components.cost = costValue;
431
418
  }
419
+ if (compatibilityValue !== null) {
420
+ // Compatibility is a penalty-only dimension. A penalized candidate loses
421
+ // a bounded fraction of its existing score; satisfied/allowed evidence
422
+ // does not receive a synthetic bonus or alter configured priority.
423
+ total = Math.max(0, total - COMPATIBILITY_WEIGHT * (1 - compatibilityValue));
424
+ components.compatibility = compatibilityValue;
425
+ }
432
426
  const score: RouteScoreEvidence = { total, components };
433
427
  const evaluated: PolicyEvaluationCandidate = {
434
428
  provider: evidence.provider,
@@ -440,6 +434,8 @@ export function evaluatePolicyProfile(
440
434
  ...(evidence.capability ? { capability: evidence.capability } : {}),
441
435
  ...(evidence.health ? { health: evidence.health } : {}),
442
436
  ...(evidence.quota ? { quota: evidence.quota } : {}),
437
+ ...(evidence.cost ? { cost: evidence.cost } : {}),
438
+ ...(compatibilityTrace ? { compatibility: compatibilityTrace } : {}),
443
439
  ...(costForCandidate ? { cost: costForCandidate } : {}),
444
440
  score,
445
441
  };
@@ -455,8 +451,6 @@ export function evaluatePolicyProfile(
455
451
  requestedModel: policyModelId(profileId),
456
452
  routeKind: "policy",
457
453
  profile: { id: profile.id, revision: profile.revision },
458
- // Flat, capped summary (truncation is flagged by the builder); per-candidate
459
- // attribution lives in each candidate's `requirements`/`exclusions`.
460
454
  requirements: candidates.flatMap(candidate => candidate.requirements).slice(0, MAX_REQUIREMENTS),
461
455
  candidates: candidates.map(candidate => ({
462
456
  provider: candidate.provider,
@@ -469,6 +463,7 @@ export function evaluatePolicyProfile(
469
463
  ...(candidate.health ? { health: candidate.health } : {}),
470
464
  ...(candidate.quota ? { quota: candidate.quota } : {}),
471
465
  ...(candidate.cost ? { cost: candidate.cost } : {}),
466
+ ...(candidate.compatibility ? { compatibility: candidate.compatibility } : {}),
472
467
  })),
473
468
  selected: selectedIndex === null
474
469
  ? { provider: candidates[0]?.provider ?? "", model: candidates[0]?.model ?? "", reason: "no-eligible-candidate" }