@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -0,0 +1,253 @@
1
+ export const COMPATIBILITY_MANIFEST_SCHEMA_VERSION = 1 as const;
2
+
3
+ export const COMPATIBILITY_DISPOSITIONS = [
4
+ "passthrough",
5
+ "translated",
6
+ "degraded",
7
+ "unsupported",
8
+ ] as const;
9
+
10
+ export type CompatibilityDisposition = typeof COMPATIBILITY_DISPOSITIONS[number];
11
+ export type CompatibilityEvidenceKind = "fixture" | "lab-scenario";
12
+
13
+ export interface CompatibilityEvidenceRefV1 {
14
+ kind: CompatibilityEvidenceKind;
15
+ id: string;
16
+ /** Exact fixture assertions that prove this claim. Required for fixture evidence. */
17
+ assertionIds?: readonly string[];
18
+ }
19
+
20
+ export interface CompatibilityClaimV1 {
21
+ /** Stable manifest-local claim id. */
22
+ id: string;
23
+ /** Stable public feature key, such as `request.previous_response_id`. */
24
+ feature: string;
25
+ disposition: CompatibilityDisposition;
26
+ summary: string;
27
+ /** Required when behavior is not byte/semantic passthrough. */
28
+ limitation?: string;
29
+ evidence: readonly CompatibilityEvidenceRefV1[];
30
+ }
31
+
32
+ export interface CompatibilitySubjectV1 {
33
+ providerId: string;
34
+ /** Exact normalized provider base URL; destination changes require a new subject/version. */
35
+ baseUrl: string;
36
+ adapterId: string;
37
+ authMode: "forward" | "key" | "oauth" | "local";
38
+ inboundProtocol: "responses" | "chat" | "messages";
39
+ upstreamProtocol: string;
40
+ /** Claims apply only to these exact wire model ids. */
41
+ modelIds: readonly string[];
42
+ }
43
+
44
+ export interface CompatibilityManifestV1 {
45
+ schemaVersion: typeof COMPATIBILITY_MANIFEST_SCHEMA_VERSION;
46
+ id: string;
47
+ version: string;
48
+ subject: CompatibilitySubjectV1;
49
+ claims: readonly CompatibilityClaimV1[];
50
+ }
51
+
52
+ const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
53
+ const FEATURE_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)+$/;
54
+ const DISPOSITIONS = new Set<string>(COMPATIBILITY_DISPOSITIONS);
55
+ const EVIDENCE_KINDS = new Set<string>(["fixture", "lab-scenario"]);
56
+
57
+ function isRecord(value: unknown): value is Record<string, unknown> {
58
+ return !!value && typeof value === "object" && !Array.isArray(value);
59
+ }
60
+
61
+ function checkKnownKeys(
62
+ value: Record<string, unknown>,
63
+ allowed: readonly string[],
64
+ path: string,
65
+ issues: string[],
66
+ ): void {
67
+ const allowedSet = new Set(allowed);
68
+ for (const key of Object.keys(value)) {
69
+ if (!allowedSet.has(key)) issues.push(`${path}${path ? "." : ""}${key} is not allowed by schema v1`);
70
+ }
71
+ }
72
+
73
+ function checkId(value: unknown, path: string, issues: string[]): value is string {
74
+ if (typeof value !== "string" || !ID_PATTERN.test(value)) {
75
+ issues.push(`${path} must match ${ID_PATTERN}`);
76
+ return false;
77
+ }
78
+ return true;
79
+ }
80
+
81
+ function checkNonBlank(value: unknown, path: string, issues: string[]): value is string {
82
+ if (typeof value !== "string" || value.trim().length === 0) {
83
+ issues.push(`${path} must be a non-blank string`);
84
+ return false;
85
+ }
86
+ return true;
87
+ }
88
+
89
+ function checkNormalizedBaseUrl(value: unknown, path: string, issues: string[]): value is string {
90
+ if (typeof value !== "string") {
91
+ issues.push(`${path} must be a normalized absolute HTTP(S) base URL`);
92
+ return false;
93
+ }
94
+ try {
95
+ const parsed = new URL(value);
96
+ if (
97
+ (parsed.protocol !== "https:" && parsed.protocol !== "http:")
98
+ || parsed.username
99
+ || parsed.password
100
+ || parsed.search
101
+ || parsed.hash
102
+ ) {
103
+ issues.push(`${path} must be a normalized absolute HTTP(S) base URL without credentials, query, or fragment`);
104
+ return false;
105
+ }
106
+ const normalizedPath = parsed.pathname.replace(/\/+$/, "");
107
+ const normalized = `${parsed.origin}${normalizedPath}`;
108
+ if (value !== normalized) {
109
+ issues.push(`${path} must be normalized as ${normalized}`);
110
+ return false;
111
+ }
112
+ return true;
113
+ } catch {
114
+ issues.push(`${path} must be a normalized absolute HTTP(S) base URL`);
115
+ return false;
116
+ }
117
+ }
118
+
119
+ function checkSortedUniqueStrings(
120
+ value: unknown,
121
+ path: string,
122
+ issues: string[],
123
+ options: { nonEmpty?: boolean } = {},
124
+ ): value is string[] {
125
+ if (!Array.isArray(value) || (options.nonEmpty === true && value.length === 0)) {
126
+ issues.push(`${path} must be${options.nonEmpty === true ? " a non-empty" : " an"} array`);
127
+ return false;
128
+ }
129
+ if (value.some(item => typeof item !== "string" || item.trim().length === 0)) {
130
+ issues.push(`${path} must contain only non-blank strings`);
131
+ return false;
132
+ }
133
+ const strings = value as string[];
134
+ if (new Set(strings).size !== strings.length) issues.push(`${path} must not contain duplicates`);
135
+ const sorted = [...strings].sort();
136
+ if (strings.some((item, index) => item !== sorted[index])) issues.push(`${path} must be sorted`);
137
+ return true;
138
+ }
139
+
140
+ function checkEvidence(value: unknown, path: string, issues: string[]): void {
141
+ if (!Array.isArray(value) || value.length === 0) {
142
+ issues.push(`${path} must be a non-empty array`);
143
+ return;
144
+ }
145
+ const seen = new Set<string>();
146
+ value.forEach((raw, index) => {
147
+ const evidencePath = `${path}[${index}]`;
148
+ if (!isRecord(raw)) {
149
+ issues.push(`${evidencePath} must be an object`);
150
+ return;
151
+ }
152
+ checkKnownKeys(raw, ["kind", "id", "assertionIds"], evidencePath, issues);
153
+ if (!EVIDENCE_KINDS.has(String(raw.kind))) {
154
+ issues.push(`${evidencePath}.kind must be fixture or lab-scenario`);
155
+ }
156
+ const id = checkId(raw.id, `${evidencePath}.id`, issues) ? raw.id : "";
157
+ const assertionIds = raw.assertionIds;
158
+ if (raw.kind === "fixture") {
159
+ checkSortedUniqueStrings(assertionIds, `${evidencePath}.assertionIds`, issues, { nonEmpty: true });
160
+ } else if (assertionIds !== undefined) {
161
+ checkSortedUniqueStrings(assertionIds, `${evidencePath}.assertionIds`, issues, { nonEmpty: true });
162
+ }
163
+ const key = `${String(raw.kind)}:${id}:${JSON.stringify(assertionIds ?? [])}`;
164
+ if (seen.has(key)) issues.push(`${evidencePath} duplicates an earlier evidence reference`);
165
+ seen.add(key);
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Validate one versioned compatibility manifest without importing provider or Lab state.
171
+ * Keeping this as a leaf lets future CLI/GUI readers load contracts without activating Lab.
172
+ */
173
+ export function compatibilityManifestIssues(value: unknown): string[] {
174
+ const issues: string[] = [];
175
+ if (!isRecord(value)) return ["manifest must be an object"];
176
+ checkKnownKeys(value, ["schemaVersion", "id", "version", "subject", "claims"], "", issues);
177
+ if (value.schemaVersion !== COMPATIBILITY_MANIFEST_SCHEMA_VERSION) {
178
+ issues.push(`schemaVersion must be ${COMPATIBILITY_MANIFEST_SCHEMA_VERSION}`);
179
+ }
180
+ checkId(value.id, "id", issues);
181
+ checkNonBlank(value.version, "version", issues);
182
+
183
+ if (!isRecord(value.subject)) {
184
+ issues.push("subject must be an object");
185
+ } else {
186
+ checkKnownKeys(
187
+ value.subject,
188
+ ["providerId", "baseUrl", "adapterId", "authMode", "inboundProtocol", "upstreamProtocol", "modelIds"],
189
+ "subject",
190
+ issues,
191
+ );
192
+ checkId(value.subject.providerId, "subject.providerId", issues);
193
+ checkNormalizedBaseUrl(value.subject.baseUrl, "subject.baseUrl", issues);
194
+ checkId(value.subject.adapterId, "subject.adapterId", issues);
195
+ if (!["forward", "key", "oauth", "local"].includes(String(value.subject.authMode))) {
196
+ issues.push("subject.authMode must be forward, key, oauth, or local");
197
+ }
198
+ if (!["responses", "chat", "messages"].includes(String(value.subject.inboundProtocol))) {
199
+ issues.push("subject.inboundProtocol must be responses, chat, or messages");
200
+ }
201
+ checkNonBlank(value.subject.upstreamProtocol, "subject.upstreamProtocol", issues);
202
+ checkSortedUniqueStrings(value.subject.modelIds, "subject.modelIds", issues, { nonEmpty: true });
203
+ }
204
+
205
+ if (!Array.isArray(value.claims) || value.claims.length === 0) {
206
+ issues.push("claims must be a non-empty array");
207
+ return issues;
208
+ }
209
+ const claimIds = new Set<string>();
210
+ const claimFeatures = new Set<string>();
211
+ value.claims.forEach((raw, index) => {
212
+ const path = `claims[${index}]`;
213
+ if (!isRecord(raw)) {
214
+ issues.push(`${path} must be an object`);
215
+ return;
216
+ }
217
+ checkKnownKeys(raw, ["id", "feature", "disposition", "summary", "limitation", "evidence"], path, issues);
218
+ const id = checkId(raw.id, `${path}.id`, issues) ? raw.id : "";
219
+ if (id && claimIds.has(id)) issues.push(`${path}.id duplicates ${id}`);
220
+ if (id) claimIds.add(id);
221
+ if (typeof raw.feature !== "string" || !FEATURE_PATTERN.test(raw.feature)) {
222
+ issues.push(`${path}.feature must be a dotted stable feature key`);
223
+ } else if (claimFeatures.has(raw.feature)) {
224
+ issues.push(`${path}.feature duplicates ${raw.feature}`);
225
+ } else {
226
+ claimFeatures.add(raw.feature);
227
+ }
228
+ if (!DISPOSITIONS.has(String(raw.disposition))) {
229
+ issues.push(`${path}.disposition is invalid`);
230
+ }
231
+ checkNonBlank(raw.summary, `${path}.summary`, issues);
232
+ if (raw.disposition !== "passthrough") {
233
+ checkNonBlank(raw.limitation, `${path}.limitation`, issues);
234
+ } else if (raw.limitation !== undefined) {
235
+ issues.push(`${path}.limitation must be omitted for passthrough claims`);
236
+ }
237
+ checkEvidence(raw.evidence, `${path}.evidence`, issues);
238
+ });
239
+ return issues;
240
+ }
241
+
242
+ function deepFreeze<T>(value: T, seen = new WeakSet<object>()): Readonly<T> {
243
+ if (!value || typeof value !== "object" || seen.has(value)) return value;
244
+ seen.add(value);
245
+ for (const nested of Object.values(value as Record<string, unknown>)) deepFreeze(nested, seen);
246
+ return Object.freeze(value);
247
+ }
248
+
249
+ export function defineCompatibilityManifest<T extends CompatibilityManifestV1>(manifest: T): Readonly<T> {
250
+ const issues = compatibilityManifestIssues(manifest);
251
+ if (issues.length > 0) throw new TypeError(`Invalid compatibility manifest ${manifest.id}: ${issues.join("; ")}`);
252
+ return deepFreeze(manifest);
253
+ }
@@ -0,0 +1,121 @@
1
+ import { defineCompatibilityManifest } from "./manifest";
2
+
3
+ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
4
+
5
+ /**
6
+ * First deliberately narrow compatibility slice: one exact native Codex-login model.
7
+ * Broader model/provider claims require their own fixtures instead of inheriting this row.
8
+ */
9
+ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
10
+ schemaVersion: 1,
11
+ id: "openai.codex-forward.gpt-5-6-sol.responses",
12
+ version: "1.3.0",
13
+ subject: {
14
+ providerId: "openai",
15
+ baseUrl: "https://chatgpt.com/backend-api/codex",
16
+ adapterId: "openai-responses",
17
+ authMode: "forward",
18
+ inboundProtocol: "responses",
19
+ upstreamProtocol: "openai-responses",
20
+ modelIds: ["gpt-5.6-sol"],
21
+ },
22
+ claims: [
23
+ {
24
+ id: "custom-tools",
25
+ feature: "request.custom_tools",
26
+ disposition: "passthrough",
27
+ summary: "Native custom-tool declarations retain their Responses representation.",
28
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-preserved"] }],
29
+ },
30
+ {
31
+ id: "prompt-cache-key",
32
+ feature: "request.prompt_cache_key",
33
+ disposition: "passthrough",
34
+ summary: "The caller's prompt cache key is forwarded unchanged.",
35
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-key-preserved"] }],
36
+ },
37
+ {
38
+ id: "previous-response-id",
39
+ feature: "request.previous_response_id",
40
+ disposition: "translated",
41
+ summary: "Continuation is owned by the proxy rather than the ChatGPT backend field.",
42
+ limitation: "OpenCodex expands remembered input locally and removes previous_response_id before dispatch.",
43
+ evidence: [
44
+ { kind: "fixture", id: FIXTURE_ID, assertionIds: ["previous-response-id-removed"] },
45
+ { kind: "lab-scenario", id: "codex-core.protocol.previous-response-replay" },
46
+ ],
47
+ },
48
+ {
49
+ id: "orphan-tool-output",
50
+ feature: "continuation.orphan_tool_output",
51
+ disposition: "degraded",
52
+ summary: "An output whose prior call is unavailable remains visible to the model.",
53
+ limitation: "The orphan output is converted to user text because the forward backend cannot accept the unmatched item.",
54
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["orphan-output-converted"] }],
55
+ },
56
+ {
57
+ id: "max-output-tokens",
58
+ feature: "request.max_output_tokens",
59
+ disposition: "unsupported",
60
+ summary: "The ChatGPT Codex forward route does not receive max_output_tokens.",
61
+ limitation: "The field is removed before dispatch to avoid a backend parameter rejection.",
62
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["max-output-tokens-removed"] }],
63
+ },
64
+ {
65
+ id: "metadata",
66
+ feature: "request.metadata",
67
+ disposition: "unsupported",
68
+ summary: "Caller metadata is not forwarded to the ChatGPT Codex backend.",
69
+ limitation: "The field is removed before dispatch.",
70
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["metadata-removed"] }],
71
+ },
72
+ {
73
+ id: "system-input-messages",
74
+ feature: "request.input.system_messages",
75
+ disposition: "translated",
76
+ summary: "Text-only input system messages are folded into top-level instructions.",
77
+ limitation: "The destination does not accept system-role input items; multimodal system messages stay unchanged rather than being dropped.",
78
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["system-message-folded", "system-message-removed"] }],
79
+ },
80
+ {
81
+ id: "truncation",
82
+ feature: "request.truncation",
83
+ disposition: "unsupported",
84
+ summary: "The ChatGPT Codex forward route does not receive truncation.",
85
+ limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
86
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["truncation-removed"] }],
87
+ },
88
+ {
89
+ id: "prompt-cache-breakpoint",
90
+ feature: "request.prompt_cache_breakpoint",
91
+ disposition: "unsupported",
92
+ summary: "Client-only prompt cache breakpoint markers do not reach the forward backend.",
93
+ limitation: "Markers are removed recursively from input within bounded depth and node budgets.",
94
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-breakpoint-removed"] }],
95
+ },
96
+ {
97
+ id: "unstored-item-reference",
98
+ feature: "continuation.item_reference",
99
+ disposition: "degraded",
100
+ summary: "Unpersisted item references are omitted from store-false continuations.",
101
+ limitation: "The referenced item cannot exist at the backend when store is false; tool call_id pairs remain unchanged.",
102
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["unstored-item-reference-removed"] }],
103
+ },
104
+ {
105
+ id: "prompt-cache-retention",
106
+ feature: "request.prompt_cache_retention",
107
+ disposition: "unsupported",
108
+ summary: "The retired retention field is not sent for gpt-5.6-sol.",
109
+ limitation: "The field is removed without inventing a replacement prompt_cache_options value.",
110
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-retention-removed"] }],
111
+ },
112
+ {
113
+ id: "prompt-cache-options",
114
+ feature: "request.prompt_cache_options",
115
+ disposition: "unsupported",
116
+ summary: "The ChatGPT Codex forward route does not receive public prompt cache options.",
117
+ limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
118
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-options-removed"] }],
119
+ },
120
+ ],
121
+ } as const);
@@ -0,0 +1,219 @@
1
+ import {
2
+ chmodSync,
3
+ lstatSync,
4
+ realpathSync,
5
+ truncateSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { dirname } from "node:path";
10
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
11
+ import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
12
+ import {
13
+ forgetEphemeralSecretPath,
14
+ hardenSecretPath,
15
+ hardenSecretPathAsync,
16
+ } from "../lib/windows-secret-acl";
17
+ import {
18
+ renameAtomicFile,
19
+ renameAtomicFileAsync,
20
+ } from "../lib/windows-atomic-replace";
21
+ import { getConfigDir } from "./paths";
22
+
23
+ let atomicSequence = 0;
24
+
25
+ /** Shared process-wide suffix source for config-owned atomic sibling files. */
26
+ export function nextAtomicTempSequence(): number {
27
+ return ++atomicSequence;
28
+ }
29
+ /** Internal error classifier shared by config backup and atomic-write paths. */
30
+ export function isMissingPathError(error: unknown): boolean {
31
+ return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
32
+ }
33
+
34
+ export type { AtomicRenameIO } from "../lib/windows-atomic-replace";
35
+ export { renameAtomicFile } from "../lib/windows-atomic-replace";
36
+
37
+ export interface AtomicWriteIO {
38
+ write: (path: string, content: string) => void;
39
+ harden: (path: string) => void;
40
+ rename: (source: string, destination: string) => void;
41
+ truncate: (path: string) => void;
42
+ unlink: (path: string) => void;
43
+ }
44
+
45
+ export class AtomicWriteResidualTempError extends Error {
46
+ constructor(readonly tempPath: string, readonly hardened = true, options?: ErrorOptions) {
47
+ super(`Atomic config write left a ${hardened ? "hardened " : ""}zero-byte temporary file`, options);
48
+ this.name = "AtomicWriteResidualTempError";
49
+ }
50
+ }
51
+
52
+ export class AtomicWriteSecretResidualError extends Error {
53
+ constructor(readonly tempPath: string, options?: ErrorOptions) {
54
+ super("Atomic config write could not scrub or remove a secret-bearing temporary file", options);
55
+ this.name = "AtomicWriteSecretResidualError";
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Resolve a write target through any symlink before the temp+rename dance so
61
+ * dotfiles-managed links survive an atomic replacement.
62
+ */
63
+ export function resolveWriteTarget(path: string): string {
64
+ try {
65
+ return realpathSync(path);
66
+ } catch (cause) {
67
+ let entry;
68
+ try {
69
+ entry = lstatSync(path);
70
+ } catch (error) {
71
+ if (isMissingPathError(error)) return path;
72
+ throw error;
73
+ }
74
+ if (entry.isSymbolicLink()) {
75
+ throw new Error(`refusing to replace unresolvable symlinked write target: ${path}`, { cause });
76
+ }
77
+ return path;
78
+ }
79
+ }
80
+
81
+ function assertResolvedTargetAllowed(path: string, target: string): void {
82
+ if (target === path) {
83
+ let realParent: string;
84
+ try {
85
+ realParent = realpathSync(dirname(target));
86
+ } catch {
87
+ return;
88
+ }
89
+ if (realParent !== dirname(target)) assertNotRealHomeUnderTest(realParent);
90
+ return;
91
+ }
92
+ assertNotRealHomeUnderTest(dirname(target));
93
+ }
94
+
95
+ export function atomicWriteFile(path: string, content: string, io: AtomicWriteIO = {
96
+ write: (target, value) => writeFileSync(target, value, { encoding: "utf-8", mode: 0o600 }),
97
+ harden: target => {
98
+ try { chmodSync(target, 0o600); } catch { /* platform may ignore chmod */ }
99
+ if (process.platform === "win32") hardenSecretPath(target, { required: true, timeoutMemoKey: path });
100
+ },
101
+ rename: renameAtomicFile,
102
+ truncate: target => truncateSync(target, 0),
103
+ unlink: unlinkSync,
104
+ }): void {
105
+ recordOwnedConfigPath(getConfigDir(), path);
106
+ const target = resolveWriteTarget(path);
107
+ assertResolvedTargetAllowed(path, target);
108
+ const tmp = `${target}.ocx.${process.pid}.${nextAtomicTempSequence()}.tmp`;
109
+ let hardened = false;
110
+ try {
111
+ io.write(tmp, content);
112
+ io.harden(tmp);
113
+ hardened = true;
114
+ io.rename(tmp, target);
115
+ forgetEphemeralSecretPath(tmp);
116
+ } catch (cause) {
117
+ let scrubbed = false;
118
+ try {
119
+ io.truncate(tmp);
120
+ scrubbed = true;
121
+ } catch (error) {
122
+ if (isMissingPathError(error)) scrubbed = true;
123
+ else {
124
+ try { io.write(tmp, ""); scrubbed = true; } catch { /* removal may still succeed */ }
125
+ }
126
+ }
127
+ let removed = false;
128
+ try {
129
+ io.unlink(tmp);
130
+ removed = true;
131
+ } catch (error) {
132
+ if (isMissingPathError(error)) removed = true;
133
+ else {
134
+ try { io.unlink(tmp); removed = true; }
135
+ catch (retryError) { if (isMissingPathError(retryError)) removed = true; }
136
+ }
137
+ }
138
+ if (!removed && !scrubbed) throw new AtomicWriteSecretResidualError(tmp, { cause });
139
+ if (!removed && !hardened) {
140
+ try { io.harden(tmp); hardened = true; } catch { /* reported below */ }
141
+ }
142
+ if (removed) forgetEphemeralSecretPath(tmp);
143
+ if (!removed) throw new AtomicWriteResidualTempError(tmp, hardened, { cause });
144
+ throw cause;
145
+ }
146
+ }
147
+
148
+ export interface AtomicWriteAsyncIO {
149
+ write: (path: string, content: string) => void | Promise<void>;
150
+ harden: (path: string) => void | Promise<void>;
151
+ rename: (source: string, destination: string) => void | Promise<void>;
152
+ truncate: (path: string) => void | Promise<void>;
153
+ unlink: (path: string) => void | Promise<void>;
154
+ }
155
+
156
+ export interface AtomicWriteAsyncTestSeam {
157
+ afterTempWrite?: (tempPath: string) => void | Promise<void>;
158
+ }
159
+
160
+ export async function atomicWriteFileAsync(
161
+ path: string,
162
+ content: string,
163
+ io?: AtomicWriteAsyncIO,
164
+ testSeam?: AtomicWriteAsyncTestSeam,
165
+ ): Promise<void> {
166
+ const effective: AtomicWriteAsyncIO = io ?? {
167
+ write: (target, value) => writeFileSync(target, value, { encoding: "utf-8", mode: 0o600 }),
168
+ harden: async target => {
169
+ try { chmodSync(target, 0o600); } catch { /* platform may ignore chmod */ }
170
+ if (process.platform === "win32") {
171
+ await hardenSecretPathAsync(target, { required: true, timeoutMemoKey: path });
172
+ }
173
+ },
174
+ rename: renameAtomicFileAsync,
175
+ truncate: target => truncateSync(target, 0),
176
+ unlink: unlinkSync,
177
+ };
178
+ const target = resolveWriteTarget(path);
179
+ assertResolvedTargetAllowed(path, target);
180
+ const tmp = `${target}.ocx.${process.pid}.${nextAtomicTempSequence()}.tmp`;
181
+ let hardened = false;
182
+ try {
183
+ await effective.write(tmp, content);
184
+ await testSeam?.afterTempWrite?.(tmp);
185
+ await effective.harden(tmp);
186
+ hardened = true;
187
+ await effective.rename(tmp, target);
188
+ forgetEphemeralSecretPath(tmp);
189
+ } catch (cause) {
190
+ let scrubbed = false;
191
+ try {
192
+ await effective.truncate(tmp);
193
+ scrubbed = true;
194
+ } catch (error) {
195
+ if (isMissingPathError(error)) scrubbed = true;
196
+ else {
197
+ try { await effective.write(tmp, ""); scrubbed = true; } catch { /* removal may still succeed */ }
198
+ }
199
+ }
200
+ let removed = false;
201
+ try {
202
+ await effective.unlink(tmp);
203
+ removed = true;
204
+ } catch (error) {
205
+ if (isMissingPathError(error)) removed = true;
206
+ else {
207
+ try { await effective.unlink(tmp); removed = true; }
208
+ catch (retryError) { if (isMissingPathError(retryError)) removed = true; }
209
+ }
210
+ }
211
+ if (!removed && !scrubbed) throw new AtomicWriteSecretResidualError(tmp, { cause });
212
+ if (!removed && !hardened) {
213
+ try { await effective.harden(tmp); hardened = true; } catch { /* reported below */ }
214
+ }
215
+ if (removed) forgetEphemeralSecretPath(tmp);
216
+ if (!removed) throw new AtomicWriteResidualTempError(tmp, hardened, { cause });
217
+ throw cause;
218
+ }
219
+ }
@@ -0,0 +1,40 @@
1
+ import { chmodSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { hardenSecretDir } from "../lib/windows-secret-acl";
5
+ import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
6
+
7
+ /**
8
+ * Expand a leading `~` in user-supplied paths without interpreting shell
9
+ * variables or `~user` forms that belong to the caller's shell.
10
+ */
11
+ export function expandUserPath(raw: string): string {
12
+ if (raw === "~") return homedir();
13
+ if (raw.startsWith("~/") || raw.startsWith("~\\")) return join(homedir(), raw.slice(2));
14
+ return raw;
15
+ }
16
+ let resolvedConfigDirCache: { raw: string | undefined; path: string } | null = null;
17
+
18
+ export function getConfigDir(): string {
19
+ const raw = process.env["OPENCODEX_HOME"]?.trim() || undefined;
20
+ if (resolvedConfigDirCache && resolvedConfigDirCache.raw === raw) return resolvedConfigDirCache.path;
21
+ const path = raw ? resolve(expandUserPath(raw)) : join(homedir(), ".opencodex");
22
+ resolvedConfigDirCache = { raw, path };
23
+ return path;
24
+ }
25
+
26
+ export function getConfigPath(): string {
27
+ return join(getConfigDir(), "config.json");
28
+ }
29
+
30
+ export function hardenConfigDir(): void {
31
+ const dir = getConfigDir();
32
+ // The guard runs before any mutation: refusing after chmod/ACL would already
33
+ // have changed the protected directory used by the test-home boundary.
34
+ assertNotRealHomeUnderTest(dir);
35
+ if (!existsSync(dir)) return;
36
+ try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
37
+ if (process.platform === "win32") {
38
+ hardenSecretDir(dir, { required: false });
39
+ }
40
+ }