Skip to content

Commit 62b7f8e

Browse files
committed
Implement conditional dotenv feature
1 parent 5013d10 commit 62b7f8e

6 files changed

Lines changed: 931 additions & 6 deletions

File tree

codex-rs/Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/arg0/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ codex-shell-escalation = { workspace = true }
2323
codex-utils-absolute-path = { workspace = true }
2424
codex-utils-home-dir = { workspace = true }
2525
dotenvy = { workspace = true }
26+
serde = { workspace = true, features = ["derive"] }
27+
serde_json = { workspace = true }
2628
tempfile = { workspace = true }
2729
tokio = { workspace = true, features = ["rt-multi-thread"] }
30+
url = { workspace = true }
2831

2932
[target.'cfg(windows)'.dependencies]
3033
codex-windows-sandbox = { workspace = true }
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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

Comments
 (0)