@golba98/codexa 1.0.1

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 (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,836 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { dirname, join, resolve } from "path";
3
+ import { normalizeWorkspaceRoot } from "../core/workspaceRoot.js";
4
+ import {
5
+ formatApprovalPolicyLabel,
6
+ formatNetworkAccessLabel,
7
+ formatPersonalityLabel,
8
+ formatSandboxModeLabel,
9
+ formatServiceTierLabel,
10
+ mergeRuntimeConfig,
11
+ type PartialRuntimeConfig,
12
+ type RuntimeApprovalPolicy,
13
+ type RuntimeConfig,
14
+ type RuntimeNetworkAccess,
15
+ type RuntimePersonality,
16
+ type RuntimeSandboxMode,
17
+ type RuntimeServiceTier,
18
+ DEFAULT_RUNTIME_CONFIG,
19
+ } from "./runtimeConfig.js";
20
+ import {
21
+ AVAILABLE_BACKENDS,
22
+ AVAILABLE_MODES,
23
+ formatBackendLabel,
24
+ formatModeLabel,
25
+ formatReasoningLabel,
26
+ getCodexConfigFile,
27
+ type AvailableBackend,
28
+ type AvailableMode,
29
+ type AvailableModel,
30
+ type ReasoningLevel,
31
+ } from "./settings.js";
32
+ import type { LaunchArgs } from "./launchArgs.js";
33
+ import { isProjectTrusted } from "./trustStore.js";
34
+ import { isRecord, serializeTomlDocument } from "./toml-serialize.js";
35
+
36
+ export const RUNTIME_FIELD_PATHS = [
37
+ "provider",
38
+ "model",
39
+ "reasoningLevel",
40
+ "mode",
41
+ "planMode",
42
+ "geminiCommandPath",
43
+ "policy.approvalPolicy",
44
+ "policy.sandboxMode",
45
+ "policy.networkAccess",
46
+ "policy.writableRoots",
47
+ "policy.serviceTier",
48
+ "policy.personality",
49
+ ] as const;
50
+
51
+ export type RuntimeFieldPath = (typeof RUNTIME_FIELD_PATHS)[number];
52
+
53
+ export type ConfigLayerStatus = "loaded" | "missing" | "blocked" | "error";
54
+
55
+ export interface ConfigLayerReport {
56
+ label: string;
57
+ status: ConfigLayerStatus;
58
+ path?: string;
59
+ reason?: string;
60
+ }
61
+
62
+ export interface LayeredConfigDiagnostics {
63
+ projectRoot: string;
64
+ projectTrusted: boolean;
65
+ selectedProfile: string | null;
66
+ selectedProfileSource: string | null;
67
+ cliOverrides: string[];
68
+ layers: ConfigLayerReport[];
69
+ ignoredEntries: string[];
70
+ fieldSources: Record<RuntimeFieldPath, string>;
71
+ }
72
+
73
+ export interface LayeredConfigResult {
74
+ runtime: RuntimeConfig;
75
+ diagnostics: LayeredConfigDiagnostics;
76
+ }
77
+
78
+ export interface ResolveLayeredConfigOptions {
79
+ workspaceRoot: string;
80
+ launchArgs: LaunchArgs;
81
+ }
82
+
83
+ interface RuntimeLayerPatch {
84
+ patch: PartialRuntimeConfig;
85
+ touchedFields: RuntimeFieldPath[];
86
+ ignoredEntries: string[];
87
+ }
88
+
89
+ interface ParsedConfigLayer {
90
+ label: string;
91
+ path: string;
92
+ data: Record<string, unknown>;
93
+ topLevelPatch: RuntimeLayerPatch;
94
+ topLevelProfile: string | null;
95
+ }
96
+
97
+ // ─── Field source tracking ─────────────────────────────────────────────────────
98
+
99
+ function createFieldSources(label: string): Record<RuntimeFieldPath, string> {
100
+ return Object.fromEntries(
101
+ RUNTIME_FIELD_PATHS.map((field) => [field, label]),
102
+ ) as Record<RuntimeFieldPath, string>;
103
+ }
104
+
105
+ function addTouchedField(target: Set<RuntimeFieldPath>, field: RuntimeFieldPath): void {
106
+ target.add(field);
107
+ }
108
+
109
+ function assignPolicyValue<T extends keyof NonNullable<PartialRuntimeConfig["policy"]>>(
110
+ patch: PartialRuntimeConfig,
111
+ key: T,
112
+ value: NonNullable<PartialRuntimeConfig["policy"]>[T],
113
+ ): void {
114
+ patch.policy = {
115
+ ...(patch.policy ?? {}),
116
+ [key]: value,
117
+ };
118
+ }
119
+
120
+ // ─── TOML patch extraction ─────────────────────────────────────────────────────
121
+
122
+ function isAbsolutePath(pathValue: string): boolean {
123
+ // Matches Windows drive-letter paths (C:\ or C:/), UNC paths (\\server), and Unix absolute paths (/).
124
+ return /^(?:[A-Za-z]:[\\/]|\\\\|\/)/.test(pathValue);
125
+ }
126
+
127
+ function resolveConfigPath(configFilePath: string, rawPath: string): string {
128
+ return normalizeWorkspaceRoot(
129
+ isAbsolutePath(rawPath) ? rawPath : resolve(dirname(configFilePath), rawPath),
130
+ );
131
+ }
132
+
133
+ function parseWritableRoots(
134
+ value: unknown,
135
+ configFilePath: string,
136
+ ignoredEntries: string[],
137
+ ): string[] | null {
138
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
139
+ ignoredEntries.push("sandbox_workspace_write.writable_roots");
140
+ return null;
141
+ }
142
+
143
+ return value.map((item) => resolveConfigPath(configFilePath, item));
144
+ }
145
+
146
+ function extractRuntimePatch(
147
+ data: Record<string, unknown>,
148
+ sourceLabel: string,
149
+ configFilePath: string,
150
+ ): RuntimeLayerPatch {
151
+ const patch: PartialRuntimeConfig = {};
152
+ const touchedFields = new Set<RuntimeFieldPath>();
153
+ const ignoredEntries: string[] = [];
154
+
155
+ if ("model" in data) {
156
+ if (typeof data.model === "string" && data.model.trim().length > 0) {
157
+ patch.model = data.model.trim() as AvailableModel;
158
+ addTouchedField(touchedFields, "model");
159
+ } else {
160
+ ignoredEntries.push("model");
161
+ }
162
+ }
163
+
164
+ if ("model_reasoning_effort" in data) {
165
+ const value = data.model_reasoning_effort;
166
+ if (typeof value === "string" && value.trim().length > 0) {
167
+ patch.reasoningLevel = value.trim() as ReasoningLevel;
168
+ addTouchedField(touchedFields, "reasoningLevel");
169
+ } else {
170
+ ignoredEntries.push("model_reasoning_effort");
171
+ }
172
+ }
173
+
174
+ const geminiCommandPath = data.geminiCommandPath ?? data.gemini_command_path;
175
+ if (geminiCommandPath !== undefined) {
176
+ if (typeof geminiCommandPath === "string" && geminiCommandPath.trim().length > 0) {
177
+ patch.geminiCommandPath = geminiCommandPath.trim();
178
+ addTouchedField(touchedFields, "geminiCommandPath");
179
+ } else {
180
+ ignoredEntries.push("gemini_command_path");
181
+ }
182
+ }
183
+
184
+ if ("approval_policy" in data) {
185
+ const value = data.approval_policy;
186
+ const validValues = ["untrusted", "on-request", "never"] as const;
187
+ if (typeof value === "string" && (validValues as readonly string[]).includes(value)) {
188
+ assignPolicyValue(patch, "approvalPolicy", value as RuntimeApprovalPolicy);
189
+ addTouchedField(touchedFields, "policy.approvalPolicy");
190
+ } else {
191
+ ignoredEntries.push("approval_policy");
192
+ }
193
+ }
194
+
195
+ if ("sandbox_mode" in data) {
196
+ const value = data.sandbox_mode;
197
+ const validValues = ["read-only", "workspace-write", "danger-full-access"] as const;
198
+ if (typeof value === "string" && (validValues as readonly string[]).includes(value)) {
199
+ assignPolicyValue(patch, "sandboxMode", value as RuntimeSandboxMode);
200
+ addTouchedField(touchedFields, "policy.sandboxMode");
201
+ } else {
202
+ ignoredEntries.push("sandbox_mode");
203
+ }
204
+ }
205
+
206
+ if ("service_tier" in data) {
207
+ const value = data.service_tier;
208
+ const validValues = ["flex", "fast"] as const;
209
+ if (typeof value === "string" && validValues.includes(value as RuntimeServiceTier)) {
210
+ assignPolicyValue(patch, "serviceTier", value as RuntimeServiceTier);
211
+ addTouchedField(touchedFields, "policy.serviceTier");
212
+ } else {
213
+ ignoredEntries.push("service_tier");
214
+ }
215
+ }
216
+
217
+ if ("personality" in data) {
218
+ const value = data.personality;
219
+ const validValues = ["none", "friendly", "pragmatic"] as const;
220
+ if (typeof value === "string" && validValues.includes(value as RuntimePersonality)) {
221
+ assignPolicyValue(patch, "personality", value as RuntimePersonality);
222
+ addTouchedField(touchedFields, "policy.personality");
223
+ } else {
224
+ ignoredEntries.push("personality");
225
+ }
226
+ }
227
+
228
+ const sandboxTable = data.sandbox_workspace_write;
229
+ if ("sandbox_workspace_write" in data) {
230
+ if (!isRecord(sandboxTable)) {
231
+ ignoredEntries.push("sandbox_workspace_write");
232
+ } else {
233
+ if ("network_access" in sandboxTable) {
234
+ if (typeof sandboxTable.network_access === "boolean") {
235
+ const networkAccess: RuntimeNetworkAccess = sandboxTable.network_access ? "enabled" : "disabled";
236
+ assignPolicyValue(patch, "networkAccess", networkAccess);
237
+ addTouchedField(touchedFields, "policy.networkAccess");
238
+ } else {
239
+ ignoredEntries.push("sandbox_workspace_write.network_access");
240
+ }
241
+ }
242
+
243
+ if ("writable_roots" in sandboxTable) {
244
+ const writableRoots = parseWritableRoots(
245
+ sandboxTable.writable_roots,
246
+ configFilePath,
247
+ ignoredEntries,
248
+ );
249
+ if (writableRoots) {
250
+ assignPolicyValue(patch, "writableRoots", writableRoots);
251
+ addTouchedField(touchedFields, "policy.writableRoots");
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ const codexaTable = data.codexa;
258
+ if ("codexa" in data) {
259
+ if (!isRecord(codexaTable)) {
260
+ ignoredEntries.push("codexa");
261
+ } else {
262
+ if ("backend" in codexaTable) {
263
+ if (
264
+ typeof codexaTable.backend === "string"
265
+ && AVAILABLE_BACKENDS.some((item) => item.id === codexaTable.backend)
266
+ ) {
267
+ patch.provider = codexaTable.backend as AvailableBackend;
268
+ addTouchedField(touchedFields, "provider");
269
+ } else {
270
+ ignoredEntries.push("codexa.backend");
271
+ }
272
+ }
273
+
274
+ if ("mode" in codexaTable) {
275
+ if (
276
+ typeof codexaTable.mode === "string"
277
+ && AVAILABLE_MODES.some((item) => item.key === codexaTable.mode)
278
+ ) {
279
+ patch.mode = codexaTable.mode as AvailableMode;
280
+ addTouchedField(touchedFields, "mode");
281
+ } else {
282
+ ignoredEntries.push("codexa.mode");
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ return {
289
+ patch,
290
+ touchedFields: Array.from(touchedFields),
291
+ ignoredEntries: ignoredEntries.map((entry) => `${sourceLabel}: ${entry}`),
292
+ };
293
+ }
294
+
295
+ // ─── Layer loading ─────────────────────────────────────────────────────────────
296
+
297
+ export function parseTomlDocument(text: string): Record<string, unknown> {
298
+ const parsed = (globalThis as { Bun?: { TOML?: { parse?: (input: string) => unknown } } }).Bun?.TOML?.parse?.(text);
299
+ if (parsed === undefined) {
300
+ throw new Error("Bun TOML parser is unavailable.");
301
+ }
302
+ return isRecord(parsed) ? parsed : {};
303
+ }
304
+
305
+ function tryLoadConfigLayer(label: string, filePath: string): ParsedConfigLayer | ConfigLayerReport {
306
+ if (!existsSync(filePath)) {
307
+ return {
308
+ label,
309
+ status: "missing",
310
+ path: filePath,
311
+ };
312
+ }
313
+
314
+ try {
315
+ const data = parseTomlDocument(readFileSync(filePath, "utf-8"));
316
+ return {
317
+ label,
318
+ path: filePath,
319
+ data,
320
+ topLevelPatch: extractRuntimePatch(data, label, filePath),
321
+ topLevelProfile: typeof data.profile === "string" && data.profile.trim().length > 0
322
+ ? data.profile.trim()
323
+ : null,
324
+ };
325
+ } catch (error) {
326
+ const message = error instanceof Error ? error.message : "Unknown TOML parse failure";
327
+ return {
328
+ label,
329
+ status: "error",
330
+ path: filePath,
331
+ reason: message,
332
+ };
333
+ }
334
+ }
335
+
336
+ function applyRuntimeLayer(
337
+ runtime: RuntimeConfig,
338
+ fieldSources: Record<RuntimeFieldPath, string>,
339
+ layer: RuntimeLayerPatch,
340
+ sourceLabel: string,
341
+ ): RuntimeConfig {
342
+ const nextRuntime = mergeRuntimeConfig(runtime, layer.patch);
343
+ for (const field of layer.touchedFields) {
344
+ fieldSources[field] = sourceLabel;
345
+ }
346
+ return nextRuntime;
347
+ }
348
+
349
+ function getProfilePatch(
350
+ layer: ParsedConfigLayer,
351
+ profileName: string,
352
+ ): RuntimeLayerPatch | null {
353
+ const profiles = layer.data.profiles;
354
+ if (!isRecord(profiles)) {
355
+ return null;
356
+ }
357
+
358
+ const profileData = profiles[profileName];
359
+ if (!isRecord(profileData)) {
360
+ return null;
361
+ }
362
+
363
+ return extractRuntimePatch(profileData, `Profile ${profileName} from ${layer.label}`, layer.path);
364
+ }
365
+
366
+ function parseTomlScalar(rawValue: string): unknown {
367
+ try {
368
+ return parseTomlDocument(`value = ${rawValue}`).value;
369
+ } catch {
370
+ return rawValue;
371
+ }
372
+ }
373
+
374
+ function extractRuntimePatchFromOverride(
375
+ workspaceRoot: string,
376
+ rawOverride: string,
377
+ ): RuntimeLayerPatch {
378
+ const separatorIndex = rawOverride.indexOf("=");
379
+ const key = separatorIndex === -1 ? rawOverride.trim() : rawOverride.slice(0, separatorIndex).trim();
380
+ const rawValue = separatorIndex === -1 ? "" : rawOverride.slice(separatorIndex + 1).trim();
381
+ const value = parseTomlScalar(rawValue);
382
+ const sourceLabel = `CLI override (${key})`;
383
+ const configPath = join(workspaceRoot, ".codex", "config.toml");
384
+
385
+ const overrideData: Record<string, unknown> = {};
386
+ switch (key) {
387
+ case "model":
388
+ overrideData.model = value;
389
+ break;
390
+ case "model_reasoning_effort":
391
+ overrideData.model_reasoning_effort = value;
392
+ break;
393
+ case "approval_policy":
394
+ overrideData.approval_policy = value;
395
+ break;
396
+ case "sandbox_mode":
397
+ overrideData.sandbox_mode = value;
398
+ break;
399
+ case "sandbox_workspace_write.network_access":
400
+ overrideData.sandbox_workspace_write = { network_access: value };
401
+ break;
402
+ case "sandbox_workspace_write.writable_roots":
403
+ overrideData.sandbox_workspace_write = { writable_roots: value };
404
+ break;
405
+ case "service_tier":
406
+ overrideData.service_tier = value;
407
+ break;
408
+ case "personality":
409
+ overrideData.personality = value;
410
+ break;
411
+ case "codexa.backend":
412
+ overrideData.codexa = { backend: value };
413
+ break;
414
+ case "codexa.mode":
415
+ overrideData.codexa = { mode: value };
416
+ break;
417
+ default:
418
+ return {
419
+ patch: {},
420
+ touchedFields: [],
421
+ ignoredEntries: [`${sourceLabel}: unsupported key`],
422
+ };
423
+ }
424
+
425
+ return extractRuntimePatch(overrideData, sourceLabel, configPath);
426
+ }
427
+
428
+ // ─── Config resolution ─────────────────────────────────────────────────────────
429
+
430
+ function findProjectRoot(workspaceRoot: string): string {
431
+ let current = normalizeWorkspaceRoot(workspaceRoot);
432
+
433
+ while (true) {
434
+ if (existsSync(join(current, ".git"))) {
435
+ return current;
436
+ }
437
+
438
+ const parent = dirname(current);
439
+ if (parent === current) {
440
+ return normalizeWorkspaceRoot(workspaceRoot);
441
+ }
442
+ current = parent;
443
+ }
444
+ }
445
+
446
+ function listProjectLayerPaths(projectRoot: string, workspaceRoot: string): string[] {
447
+ const normalizedProjectRoot = normalizeWorkspaceRoot(projectRoot);
448
+ const normalizedWorkspaceRoot = normalizeWorkspaceRoot(workspaceRoot);
449
+ const paths: string[] = [];
450
+ let current = normalizedWorkspaceRoot;
451
+
452
+ while (true) {
453
+ paths.unshift(join(current, ".codex", "config.toml"));
454
+ if (current === normalizedProjectRoot) {
455
+ break;
456
+ }
457
+ const parent = dirname(current);
458
+ if (parent === current) {
459
+ break;
460
+ }
461
+ current = parent;
462
+ }
463
+
464
+ return paths;
465
+ }
466
+
467
+ export function resolveLayeredConfig(options: ResolveLayeredConfigOptions): LayeredConfigResult {
468
+ // Config resolution order (each layer wins over the previous):
469
+ // 1. Built-in defaults (DEFAULT_RUNTIME_CONFIG)
470
+ // 2. User config (~/.codex/config.toml)
471
+ // 3. Project config (.codex/config.toml, only when project is trusted)
472
+ // 4. Profile patch ([profiles.<name>] from any loaded layer)
473
+ // 5. CLI overrides (--config key=value flags)
474
+ const workspaceRoot = normalizeWorkspaceRoot(options.workspaceRoot);
475
+ const projectRoot = findProjectRoot(workspaceRoot);
476
+ const projectTrusted = isProjectTrusted(projectRoot);
477
+ const diagnostics: LayeredConfigDiagnostics = {
478
+ projectRoot,
479
+ projectTrusted,
480
+ selectedProfile: null,
481
+ selectedProfileSource: null,
482
+ cliOverrides: [...options.launchArgs.configOverrides],
483
+ layers: [{ label: "Built-in defaults", status: "loaded" }],
484
+ ignoredEntries: [],
485
+ fieldSources: createFieldSources("Built-in defaults"),
486
+ };
487
+
488
+ let runtime = DEFAULT_RUNTIME_CONFIG;
489
+ const loadedLayers: ParsedConfigLayer[] = [];
490
+ // The last profile name found in any config file. CLI --profile takes priority over this.
491
+ let configFileProfile: { name: string; source: string } | null = null;
492
+
493
+ const userConfigFile = getCodexConfigFile();
494
+ const userLayer = tryLoadConfigLayer("User config", userConfigFile);
495
+ if ("data" in userLayer) {
496
+ runtime = applyRuntimeLayer(runtime, diagnostics.fieldSources, userLayer.topLevelPatch, "User config");
497
+ diagnostics.layers.push({ label: "User config", status: "loaded", path: userLayer.path });
498
+ diagnostics.ignoredEntries.push(...userLayer.topLevelPatch.ignoredEntries);
499
+ loadedLayers.push(userLayer);
500
+ if (userLayer.topLevelProfile) {
501
+ configFileProfile = { name: userLayer.topLevelProfile, source: "User config" };
502
+ }
503
+ } else {
504
+ diagnostics.layers.push(userLayer);
505
+ }
506
+
507
+ const projectLayerPaths = listProjectLayerPaths(projectRoot, workspaceRoot)
508
+ .filter((filePath) => existsSync(filePath));
509
+
510
+ if (projectLayerPaths.length === 0) {
511
+ diagnostics.layers.push({
512
+ label: "Project config",
513
+ status: "missing",
514
+ path: join(projectRoot, ".codex", "config.toml"),
515
+ });
516
+ } else if (!projectTrusted) {
517
+ for (const filePath of projectLayerPaths) {
518
+ diagnostics.layers.push({
519
+ label: "Project config",
520
+ status: "blocked",
521
+ path: filePath,
522
+ reason: "project is untrusted",
523
+ });
524
+ }
525
+ } else {
526
+ for (const filePath of projectLayerPaths) {
527
+ const relativeLabel = filePath === join(projectRoot, ".codex", "config.toml")
528
+ ? "Project config"
529
+ : `Project config (${dirname(dirname(filePath)).slice(projectRoot.length + 1) || "."})`;
530
+ const layer = tryLoadConfigLayer(relativeLabel, filePath);
531
+ if ("data" in layer) {
532
+ runtime = applyRuntimeLayer(runtime, diagnostics.fieldSources, layer.topLevelPatch, layer.label);
533
+ diagnostics.layers.push({ label: layer.label, status: "loaded", path: layer.path });
534
+ diagnostics.ignoredEntries.push(...layer.topLevelPatch.ignoredEntries);
535
+ loadedLayers.push(layer);
536
+ if (layer.topLevelProfile) {
537
+ configFileProfile = { name: layer.topLevelProfile, source: layer.label };
538
+ }
539
+ } else {
540
+ diagnostics.layers.push(layer);
541
+ }
542
+ }
543
+ }
544
+
545
+ if (options.launchArgs.profile) {
546
+ diagnostics.selectedProfile = options.launchArgs.profile;
547
+ diagnostics.selectedProfileSource = "CLI --profile";
548
+ } else if (configFileProfile) {
549
+ diagnostics.selectedProfile = configFileProfile.name;
550
+ diagnostics.selectedProfileSource = configFileProfile.source;
551
+ }
552
+
553
+ if (diagnostics.selectedProfile) {
554
+ let matchedProfile = false;
555
+ for (const layer of loadedLayers) {
556
+ const profilePatch = getProfilePatch(layer, diagnostics.selectedProfile);
557
+ if (!profilePatch) {
558
+ continue;
559
+ }
560
+
561
+ matchedProfile = true;
562
+ const label = `Profile ${diagnostics.selectedProfile} from ${layer.label}`;
563
+ runtime = applyRuntimeLayer(runtime, diagnostics.fieldSources, profilePatch, label);
564
+ diagnostics.layers.push({
565
+ label,
566
+ status: "loaded",
567
+ path: layer.path,
568
+ });
569
+ diagnostics.ignoredEntries.push(...profilePatch.ignoredEntries);
570
+ }
571
+
572
+ if (!matchedProfile) {
573
+ diagnostics.ignoredEntries.push(`Selected profile not found: ${diagnostics.selectedProfile}`);
574
+ }
575
+ }
576
+
577
+ for (const rawOverride of options.launchArgs.configOverrides) {
578
+ const overridePatch = extractRuntimePatchFromOverride(workspaceRoot, rawOverride);
579
+ diagnostics.ignoredEntries.push(...overridePatch.ignoredEntries);
580
+ if (overridePatch.touchedFields.length === 0) {
581
+ continue;
582
+ }
583
+
584
+ runtime = applyRuntimeLayer(runtime, diagnostics.fieldSources, overridePatch, `CLI override (${rawOverride})`);
585
+ diagnostics.layers.push({
586
+ label: `CLI override`,
587
+ status: "loaded",
588
+ reason: rawOverride,
589
+ });
590
+ }
591
+
592
+ return {
593
+ runtime,
594
+ diagnostics,
595
+ };
596
+ }
597
+
598
+ // ─── Runtime override helpers ──────────────────────────────────────────────────
599
+
600
+ function getTouchedFieldsFromPatch(patch: PartialRuntimeConfig): RuntimeFieldPath[] {
601
+ const touched = new Set<RuntimeFieldPath>();
602
+
603
+ if (patch.provider !== undefined) touched.add("provider");
604
+ if (patch.model !== undefined) touched.add("model");
605
+ if (patch.reasoningLevel !== undefined) touched.add("reasoningLevel");
606
+ if (patch.mode !== undefined) touched.add("mode");
607
+ if (patch.planMode !== undefined) touched.add("planMode");
608
+ if (patch.geminiCommandPath !== undefined) touched.add("geminiCommandPath");
609
+ if (patch.policy?.approvalPolicy !== undefined) touched.add("policy.approvalPolicy");
610
+ if (patch.policy?.sandboxMode !== undefined) touched.add("policy.sandboxMode");
611
+ if (patch.policy?.networkAccess !== undefined) touched.add("policy.networkAccess");
612
+ if (patch.policy?.writableRoots !== undefined) touched.add("policy.writableRoots");
613
+ if (patch.policy?.serviceTier !== undefined) touched.add("policy.serviceTier");
614
+ if (patch.policy?.personality !== undefined) touched.add("policy.personality");
615
+
616
+ return Array.from(touched);
617
+ }
618
+
619
+ export function applyLayeredRuntimeOverride(
620
+ base: LayeredConfigResult,
621
+ override: PartialRuntimeConfig,
622
+ sourceLabel: string,
623
+ ): LayeredConfigResult {
624
+ const touchedFields = getTouchedFieldsFromPatch(override);
625
+ if (touchedFields.length === 0) {
626
+ return base;
627
+ }
628
+
629
+ const runtime = mergeRuntimeConfig(base.runtime, override);
630
+ const fieldSources = { ...base.diagnostics.fieldSources };
631
+ for (const field of touchedFields) {
632
+ fieldSources[field] = sourceLabel;
633
+ }
634
+
635
+ return {
636
+ runtime,
637
+ diagnostics: {
638
+ ...base.diagnostics,
639
+ fieldSources,
640
+ layers: [
641
+ ...base.diagnostics.layers,
642
+ {
643
+ label: sourceLabel,
644
+ status: "loaded",
645
+ },
646
+ ],
647
+ },
648
+ };
649
+ }
650
+
651
+ // ─── Diagnostics formatting ────────────────────────────────────────────────────
652
+
653
+ function formatRuntimeFieldValue(runtime: RuntimeConfig, field: RuntimeFieldPath): string {
654
+ switch (field) {
655
+ case "provider":
656
+ return formatBackendLabel(runtime.provider);
657
+ case "model":
658
+ return runtime.model;
659
+ case "reasoningLevel":
660
+ return formatReasoningLabel(runtime.reasoningLevel);
661
+ case "mode":
662
+ return formatModeLabel(runtime.mode);
663
+ case "planMode":
664
+ return runtime.planMode ? "Enabled" : "Disabled";
665
+ case "geminiCommandPath":
666
+ return runtime.geminiCommandPath ?? "none";
667
+ case "policy.approvalPolicy":
668
+ return formatApprovalPolicyLabel(runtime.policy.approvalPolicy);
669
+ case "policy.sandboxMode":
670
+ return formatSandboxModeLabel(runtime.policy.sandboxMode);
671
+ case "policy.networkAccess":
672
+ return formatNetworkAccessLabel(runtime.policy.networkAccess);
673
+ case "policy.writableRoots":
674
+ return runtime.policy.writableRoots.length > 0
675
+ ? runtime.policy.writableRoots.join(", ")
676
+ : "none";
677
+ case "policy.serviceTier":
678
+ return formatServiceTierLabel(runtime.policy.serviceTier);
679
+ case "policy.personality":
680
+ return formatPersonalityLabel(runtime.policy.personality);
681
+ default:
682
+ return "";
683
+ }
684
+ }
685
+
686
+ export function formatLayeredConfigStatus(result: LayeredConfigResult): string {
687
+ const { diagnostics, runtime } = result;
688
+ const lines = [
689
+ "Config status:",
690
+ ` Project root: ${diagnostics.projectRoot}`,
691
+ ` Project trust: ${diagnostics.projectTrusted ? "Trusted" : "Untrusted"}`,
692
+ ` Selected profile: ${diagnostics.selectedProfile ? `${diagnostics.selectedProfile} (${diagnostics.selectedProfileSource ?? "unknown"})` : "none"}`,
693
+ ` CLI overrides: ${diagnostics.cliOverrides.length > 0 ? diagnostics.cliOverrides.join(", ") : "none"}`,
694
+ " Layers:",
695
+ ];
696
+
697
+ for (const layer of diagnostics.layers) {
698
+ const detail = [
699
+ layer.path ? `path ${layer.path}` : null,
700
+ layer.reason ?? null,
701
+ ].filter(Boolean).join("; ");
702
+ lines.push(
703
+ ` - ${layer.label}: ${layer.status}${detail ? ` (${detail})` : ""}`,
704
+ );
705
+ }
706
+
707
+ lines.push(" Winning sources:");
708
+ for (const field of RUNTIME_FIELD_PATHS) {
709
+ lines.push(
710
+ ` - ${field}: ${formatRuntimeFieldValue(runtime, field)} <- ${diagnostics.fieldSources[field]}`,
711
+ );
712
+ }
713
+
714
+ if (diagnostics.ignoredEntries.length > 0) {
715
+ lines.push(" Ignored entries:");
716
+ for (const entry of diagnostics.ignoredEntries) {
717
+ lines.push(` - ${entry}`);
718
+ }
719
+ }
720
+
721
+ return lines.join("\n");
722
+ }
723
+
724
+ // ─── TOML merge helpers ────────────────────────────────────────────────────────
725
+
726
+ function cloneTomlValue(value: unknown): unknown {
727
+ if (Array.isArray(value)) {
728
+ return value.map((item) => cloneTomlValue(item));
729
+ }
730
+
731
+ if (isRecord(value)) {
732
+ return Object.fromEntries(
733
+ Object.entries(value).map(([key, item]) => [key, cloneTomlValue(item)]),
734
+ );
735
+ }
736
+
737
+ return value;
738
+ }
739
+
740
+ function ensureTable(root: Record<string, unknown>, path: readonly string[]): Record<string, unknown> {
741
+ let current = root;
742
+ for (const segment of path) {
743
+ const next = current[segment];
744
+ if (!isRecord(next)) {
745
+ current[segment] = {};
746
+ }
747
+ current = current[segment] as Record<string, unknown>;
748
+ }
749
+ return current;
750
+ }
751
+
752
+ function getNestedValue(root: Record<string, unknown>, path: readonly string[]): unknown {
753
+ let current: unknown = root;
754
+ for (const segment of path) {
755
+ if (!isRecord(current) || !(segment in current)) {
756
+ return undefined;
757
+ }
758
+ current = current[segment];
759
+ }
760
+ return current;
761
+ }
762
+
763
+ function setNestedValue(root: Record<string, unknown>, path: readonly string[], value: unknown): void {
764
+ const table = ensureTable(root, path.slice(0, -1));
765
+ table[path[path.length - 1]!] = cloneTomlValue(value);
766
+ }
767
+
768
+ export function mergeRuntimeIntoTomlConfig(
769
+ currentData: Record<string, unknown>,
770
+ runtime: RuntimeConfig,
771
+ ): Record<string, unknown> {
772
+ const nextData = cloneTomlValue(currentData) as Record<string, unknown>;
773
+ const defaultRuntime = DEFAULT_RUNTIME_CONFIG;
774
+ const entries: Array<{ path: string[]; value: unknown; shouldWrite: boolean }> = [
775
+ { path: ["model"], value: runtime.model, shouldWrite: runtime.model !== defaultRuntime.model },
776
+ {
777
+ path: ["model_reasoning_effort"],
778
+ value: runtime.reasoningLevel,
779
+ shouldWrite: runtime.reasoningLevel !== defaultRuntime.reasoningLevel,
780
+ },
781
+ {
782
+ path: ["approval_policy"],
783
+ value: runtime.policy.approvalPolicy,
784
+ shouldWrite: runtime.policy.approvalPolicy !== defaultRuntime.policy.approvalPolicy,
785
+ },
786
+ {
787
+ path: ["sandbox_mode"],
788
+ value: runtime.policy.sandboxMode,
789
+ shouldWrite: runtime.policy.sandboxMode !== defaultRuntime.policy.sandboxMode,
790
+ },
791
+ {
792
+ path: ["sandbox_workspace_write", "network_access"],
793
+ value: runtime.policy.networkAccess === "enabled",
794
+ shouldWrite: runtime.policy.networkAccess !== defaultRuntime.policy.networkAccess,
795
+ },
796
+ {
797
+ path: ["sandbox_workspace_write", "writable_roots"],
798
+ value: runtime.policy.writableRoots,
799
+ shouldWrite: runtime.policy.writableRoots.length > 0,
800
+ },
801
+ {
802
+ path: ["service_tier"],
803
+ value: runtime.policy.serviceTier,
804
+ shouldWrite: runtime.policy.serviceTier !== defaultRuntime.policy.serviceTier,
805
+ },
806
+ {
807
+ path: ["personality"],
808
+ value: runtime.policy.personality,
809
+ shouldWrite: runtime.policy.personality !== defaultRuntime.policy.personality,
810
+ },
811
+ {
812
+ path: ["codexa", "backend"],
813
+ value: runtime.provider,
814
+ shouldWrite: runtime.provider !== defaultRuntime.provider,
815
+ },
816
+ {
817
+ path: ["codexa", "mode"],
818
+ value: runtime.mode,
819
+ shouldWrite: runtime.mode !== defaultRuntime.mode,
820
+ },
821
+ ];
822
+
823
+ for (const entry of entries) {
824
+ if (!entry.shouldWrite) {
825
+ continue;
826
+ }
827
+
828
+ if (getNestedValue(nextData, entry.path) !== undefined) {
829
+ continue;
830
+ }
831
+
832
+ setNestedValue(nextData, entry.path, entry.value);
833
+ }
834
+
835
+ return nextData;
836
+ }