|
| 1 | +# Conditional dotenv overlays |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +This document describes the initial implementation of conditional dotenv overlays in the Codex |
| 6 | +startup path. |
| 7 | + |
| 8 | +## Goal |
| 9 | + |
| 10 | +Allow a packaged Codex binary to select additional environment variables at process startup based |
| 11 | +on local conditions. The selected variables must be visible before Codex creates its main thread, |
| 12 | +Tokio runtime, network clients, workers, or sessions. |
| 13 | + |
| 14 | +The motivating use case is enabling proxy variables only when an enterprise proxy is reachable, |
| 15 | +but the mechanism is intentionally not proxy-specific. |
| 16 | + |
| 17 | +## File model |
| 18 | + |
| 19 | +Codex continues to load `${CODEX_HOME}/.env` unconditionally. It then discovers regular files in |
| 20 | +`CODEX_HOME` whose names begin with `.env.`. Conditional files are processed in lexicographic |
| 21 | +filename order. |
| 22 | + |
| 23 | +Every conditional file must begin with a condition directive as its first non-empty line: |
| 24 | + |
| 25 | +```dotenv |
| 26 | +# codex-env-if: {"type":"tcp_connect","from":"HTTPS_PROXY","timeout_ms":500} |
| 27 | +
|
| 28 | +HTTPS_PROXY=http://proxy.example.com:8080 |
| 29 | +HTTP_PROXY=http://proxy.example.com:8080 |
| 30 | +ALL_PROXY=http://proxy.example.com:8080 |
| 31 | +NO_PROXY=localhost,127.0.0.1,.example.com |
| 32 | +TEST_VAR=APPLES |
| 33 | +``` |
| 34 | + |
| 35 | +The directive is a dotenv comment, so the remainder of the file stays compatible with ordinary |
| 36 | +dotenv tooling. Codex parses the JSON payload before passing the file through `dotenvy`. |
| 37 | + |
| 38 | +Files without the directive are ignored. This prevents unrelated files such as `.env.example` or |
| 39 | +backups from being applied accidentally. |
| 40 | + |
| 41 | +## Conditions |
| 42 | + |
| 43 | +The initial implementation supports these leaf evaluators: |
| 44 | + |
| 45 | +- `tcp_connect`: attempt a TCP connection. The endpoint can come from a variable declared in the |
| 46 | + same overlay via `from`, or from explicit `host` and `port` fields. Proxy URLs with credentials |
| 47 | + are parsed without logging their values. |
| 48 | +- `file_exists`: test a file path. Relative paths are resolved against `CODEX_HOME`. |
| 49 | +- `env_equals`: compare an already-applied process environment variable with a string. |
| 50 | +- `os`: compare against Rust's operating-system identifier, such as `macos`, `linux`, or `windows`. |
| 51 | + |
| 52 | +Conditions can be composed with `not`, `all`, and `any`: |
| 53 | + |
| 54 | +```dotenv |
| 55 | +# codex-env-if: {"all":[{"type":"os","value":"macos"},{"not":{"type":"env_equals","name":"DISABLE_CORPORATE_ENV","value":"1"}}]} |
| 56 | +``` |
| 57 | + |
| 58 | +Condition nesting and list sizes are bounded. Invalid conditions fail closed: Codex skips the |
| 59 | +overlay and reports a warning without printing environment values. |
| 60 | + |
| 61 | +The `tcp_connect` timeout is bounded. Hostname resolution uses the operating system's synchronous |
| 62 | +resolver and can take longer than the connection timeout; using an IP address avoids that |
| 63 | +uncertainty when a strictly bounded probe is required. |
| 64 | + |
| 65 | +## Applying and unsetting values |
| 66 | + |
| 67 | +When a condition passes, Codex first applies an optional unset directive and then applies the |
| 68 | +dotenv assignments: |
| 69 | + |
| 70 | +```dotenv |
| 71 | +# codex-env-if: {"not":{"type":"tcp_connect","host":"proxy.example.com","port":8080,"timeout_ms":500}} |
| 72 | +# codex-env-unset: ["HTTPS_PROXY","HTTP_PROXY","ALL_PROXY","NO_PROXY"] |
| 73 | +
|
| 74 | +TEST_VAR=APPLES |
| 75 | +``` |
| 76 | + |
| 77 | +Later passing overlays override values from `.env` and earlier overlays. A failing overlay makes no |
| 78 | +changes. |
| 79 | + |
| 80 | +As with the existing `.env` loader, conditional overlays cannot set or unset variables whose names |
| 81 | +begin with `CODEX_`, compared case-insensitively. The mechanism otherwise supports arbitrary |
| 82 | +environment-variable names and is not limited to proxy settings. |
| 83 | + |
| 84 | +## Startup lifecycle |
| 85 | + |
| 86 | +Conditional overlays are loaded from `codex_arg0::arg0_dispatch`, at the same single-threaded point |
| 87 | +where `.env` is currently loaded: |
| 88 | + |
| 89 | +1. Load `.env`. |
| 90 | +2. Discover, parse, evaluate, and apply matching `.env.*` overlays. |
| 91 | +3. Prepare the packaged helper `PATH` entries. |
| 92 | +4. Spawn the `codex-main` thread. |
| 93 | +5. Construct Tokio and initialize the selected Codex entry point. |
| 94 | + |
| 95 | +Evaluators are synchronous and do not create threads or a runtime. The TCP probe is the only |
| 96 | +network operation supported by the initial evaluators, and it does not depend on proxy environment |
| 97 | +variables. |
| 98 | + |
| 99 | +Conditions are evaluated once per process. Users restart Codex after moving between networks. |
| 100 | + |
| 101 | +## Security and managed delivery |
| 102 | + |
| 103 | +Discovery is limited to `CODEX_HOME`; project-local `.env.*` files are never considered. The |
| 104 | +initial evaluator set is declarative and cannot execute commands. |
| 105 | + |
| 106 | +An arbitrary command evaluator is deliberately deferred. Automatically running user-controlled or |
| 107 | +cloud-delivered code before the Codex security and approval systems initialize requires explicit |
| 108 | +trust semantics, bounded execution, and—for managed delivery—a server-authenticated signature over |
| 109 | +the evaluator and its metadata. |
| 110 | + |
| 111 | +Cloud or MDM delivery can write the dotenv overlays as data. If delivery happens after the current |
| 112 | +Codex process has started, Codex must be restarted before the new overlay can affect that process. |
| 113 | + |
| 114 | +## Validation |
| 115 | + |
| 116 | +The minimum end-to-end validation is: |
| 117 | + |
| 118 | +1. Put `TEST_VAR=APPLES` in a passing conditional overlay. |
| 119 | +2. Start the packaged Codex binary normally. |
| 120 | +3. From a local Codex thread, run a command that prints `TEST_VAR` and verify that it is `APPLES`. |
| 121 | +4. Repeat with the condition failing and verify that the overlay is absent. |
| 122 | +5. Test proxy and direct networks with a full Codex restart between them, confirming that proxy |
| 123 | + variables are present only when intended. |
| 124 | + |
| 125 | +## Enterprise deployment |
| 126 | + |
| 127 | +The local TUI and the packaged Codex Desktop App use the same per-user `CODEX_HOME`. Admins should |
| 128 | +deploy the overlay to one of these locations: |
| 129 | + |
| 130 | +| Platform | Example path | |
| 131 | +| --- | --- | |
| 132 | +| macOS and Linux | `~/.codex/.env.blackrock-proxy` | |
| 133 | +| Windows | `%USERPROFILE%\.codex\.env.blackrock-proxy` | |
| 134 | +| Custom Codex home | `$CODEX_HOME/.env.blackrock-proxy` | |
| 135 | + |
| 136 | +The Desktop App's bundled Codex backend reads the overlay through the shared startup path. For a |
| 137 | +TUI connected to a remote app-server, the overlay must instead be deployed to the user account and |
| 138 | +`CODEX_HOME` on the machine running the app-server, because that process performs the relevant |
| 139 | +network operations. |
| 140 | + |
| 141 | +On managed macOS devices, administrators can use MDM or Jamf managed-file deployment, or an |
| 142 | +administrator-installed package that targets each user's Codex home. On Windows, Group Policy |
| 143 | +Preferences or Intune can create the per-user `.codex` directory and copy the overlay while running |
| 144 | +in the user's context. Deployment does not require the user to run a script or launch Codex from a |
| 145 | +terminal. Codex must be fully restarted after deployment or after the user changes networks. |
| 146 | + |
| 147 | +This is a user-level startup file, not an enforced managed-policy location. The initial |
| 148 | +implementation does not verify a signature or prevent the user from modifying the overlay; MDM or |
| 149 | +GPO controls delivery only. If the file must be tamper-resistant or server-authenticated, that |
| 150 | +requires a separate managed-file location and signature-verification design. `CODEX_HOME` must also |
| 151 | +be established before Codex starts and cannot be selected from `.env` or `.env.*` itself. |
| 152 | + |
| 153 | +## Blackrock MVP |
| 154 | + |
| 155 | +The Blackrock MVP has one purpose: enable the corporate proxy when its TCP endpoint is reachable |
| 156 | +from the device, and remove the proxy environment variables when that endpoint is unreachable. The |
| 157 | +proxy is required only while the device is on the corporate network. |
| 158 | + |
| 159 | +### Required condition and actions |
| 160 | + |
| 161 | +The MVP needs only the `tcp_connect` condition and a direct negation of that condition. It does not |
| 162 | +need `file_exists`, `env_equals`, `os`, `all`, `any`, or arbitrarily nested condition trees. The two |
| 163 | +supported condition shapes are: |
| 164 | + |
| 165 | +```json |
| 166 | +{"type":"tcp_connect","host":"proxy.example.com","port":8080,"timeout_ms":500} |
| 167 | +{"not":{"type":"tcp_connect","host":"proxy.example.com","port":8080,"timeout_ms":500}} |
| 168 | +``` |
| 169 | + |
| 170 | +The positive condition may also use `from` to read a proxy URL from an assignment in the same |
| 171 | +overlay. This preserves the documented proxy configuration without duplicating the endpoint: |
| 172 | + |
| 173 | +```json |
| 174 | +{"type":"tcp_connect","from":"HTTPS_PROXY","timeout_ms":500} |
| 175 | +``` |
| 176 | + |
| 177 | +When an overlay condition passes, Codex must support both actions already defined by this format: |
| 178 | + |
| 179 | +- Remove each variable named by `# codex-env-unset`. |
| 180 | +- Apply the overlay's dotenv assignments after the removals. |
| 181 | + |
| 182 | +A failing condition makes no changes. Set and unset operations must both continue to reject names |
| 183 | +beginning with `CODEX_`, compared case-insensitively. |
| 184 | + |
| 185 | +### Blackrock deployment |
| 186 | + |
| 187 | +With the current apply-on-match semantics, setting variables on a successful check and unsetting |
| 188 | +them on a failed check requires two mutually exclusive overlays. For example, the on-network |
| 189 | +overlay can be deployed as `.env.10-blackrock-proxy-on`: |
| 190 | + |
| 191 | +```dotenv |
| 192 | +# codex-env-if: {"type":"tcp_connect","from":"HTTPS_PROXY","timeout_ms":500} |
| 193 | +
|
| 194 | +HTTPS_PROXY=http://proxy.example.com:8080 |
| 195 | +HTTP_PROXY=http://proxy.example.com:8080 |
| 196 | +ALL_PROXY=http://proxy.example.com:8080 |
| 197 | +NO_PROXY=localhost,127.0.0.1,.example.com |
| 198 | +``` |
| 199 | + |
| 200 | +The off-network overlay can be deployed as `.env.20-blackrock-proxy-off`: |
| 201 | + |
| 202 | +```dotenv |
| 203 | +# codex-env-if: {"not":{"type":"tcp_connect","host":"proxy.example.com","port":8080,"timeout_ms":500}} |
| 204 | +# codex-env-unset: ["HTTPS_PROXY","HTTP_PROXY","ALL_PROXY","NO_PROXY"] |
| 205 | +``` |
| 206 | + |
| 207 | +The managed deployment must replace the example endpoint and variable list with the actual |
| 208 | +Blackrock proxy configuration. It should include every proxy variable that the deployment may set |
| 209 | +or inherit and that must not remain active off the corporate network. |
| 210 | + |
| 211 | +The resulting behavior is: |
| 212 | + |
| 213 | +| Device network | Positive overlay | Negative overlay | Final proxy environment | |
| 214 | +| --- | --- | --- | --- | |
| 215 | +| Corporate proxy reachable | Sets the managed proxy values | Makes no changes | Proxy variables are present | |
| 216 | +| Corporate proxy unreachable | Makes no changes | Removes the managed proxy names | Proxy variables are absent | |
| 217 | + |
| 218 | +Each overlay is evaluated independently, so this two-file configuration performs two TCP checks at |
| 219 | +startup. If deployment must use exactly one managed file and one TCP check, the format needs an |
| 220 | +explicit false-branch action such as `unset-if-false`; the current format does not provide one. |
| 221 | + |
| 222 | +### Defensive behavior retained for the MVP |
| 223 | + |
| 224 | +The narrowed implementation must retain the defenses that apply to TCP-based overlays: |
| 225 | + |
| 226 | +- Run during single-threaded startup, before network clients, workers, sessions, or Tokio are |
| 227 | + created. |
| 228 | +- Discover overlays only in the startup `CODEX_HOME`, process them in deterministic filename order, |
| 229 | + and do not let `.env` or an overlay redirect further discovery. |
| 230 | +- Parse conditions and dotenv assignments before applying them. Invalid overlays fail closed, emit |
| 231 | + a warning without exposing environment values or proxy credentials, and do not prevent other |
| 232 | + overlays from being considered. |
| 233 | +- Use a default TCP connection timeout, enforce a small maximum timeout, and share the timeout |
| 234 | + budget across all resolved socket addresses for one check. |
| 235 | +- Continue filtering `CODEX_` names for both set and unset actions. |
| 236 | +- Evaluate conditions only once per process; moving between networks requires a full Codex restart. |
| 237 | + |
| 238 | +The connection timeout does not bound synchronous DNS resolution. A hostname lookup can therefore |
| 239 | +take longer than `timeout_ms`; an IP address should be used when the probe needs a strict total time |
| 240 | +bound. |
| 241 | + |
| 242 | +### MVP validation |
| 243 | + |
| 244 | +Validation should exercise the complete Blackrock behavior rather than the deferred general |
| 245 | +condition engine: |
| 246 | + |
| 247 | +1. Start Codex with the proxy reachable and verify that all managed proxy variables have the |
| 248 | + configured values before any network client is initialized. |
| 249 | +2. Start Codex with the proxy unreachable and inherited proxy variables present, then verify that |
| 250 | + every managed proxy variable is absent. |
| 251 | +3. Verify the default and maximum TCP timeout behavior. |
| 252 | +4. Verify that malformed conditions fail closed and that `CODEX_` variables cannot be set or unset. |
| 253 | +5. Repeat the on-network and off-network checks with a full process restart between them. |
0 commit comments