codex-api-proxy 0.1.0__tar.gz → 0.1.1__tar.gz

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 (25) hide show
  1. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/PKG-INFO +5 -2
  2. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/README.md +4 -1
  3. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/pyproject.toml +1 -1
  4. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/__init__.py +1 -1
  5. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/app_server_runner.py +7 -3
  6. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/cli.py +19 -0
  7. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/codex_runner.py +4 -1
  8. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/config.py +2 -0
  9. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/main.py +2 -0
  10. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/PKG-INFO +5 -2
  11. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_api.py +10 -1
  12. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_app_server_runner.py +28 -0
  13. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_cli.py +36 -3
  14. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_codex_runner.py +7 -5
  15. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_config.py +2 -0
  16. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/setup.cfg +0 -0
  17. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/prompt.py +0 -0
  18. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/schemas.py +0 -0
  19. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/SOURCES.txt +0 -0
  20. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/dependency_links.txt +0 -0
  21. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/entry_points.txt +0 -0
  22. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/requires.txt +0 -0
  23. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/top_level.txt +0 -0
  24. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_prompt.py +0 -0
  25. {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_release_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-api-proxy
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Local OpenAI-compatible HTTP proxy backed by Codex CLI
5
5
  Author: codex-api-proxy contributors
6
6
  License-Expression: MIT
@@ -40,8 +40,9 @@ If you start with `--host 0.0.0.0` or another non-loopback bind address without
40
40
  With the default `exec` engine, Codex subprocesses are launched with `--ignore-user-config` and `--ignore-rules`. This prevents proxy requests from loading user Codex config, MCP servers, plugins, skills, and rule files.
41
41
 
42
42
  Codex subprocesses also use `--sandbox read-only` and `--ephemeral` by default. This keeps calls closer to one-shot model calls where the caller owns conversation context.
43
+ Use `--agent` only for trusted clients when you want Codex to use agent tools and create or modify files under the selected workspace.
43
44
 
44
- The experimental `app-server` engine uses Codex's long-lived app-server protocol to reduce process startup latency and stream assistant deltas. Each API request starts a fresh Codex thread and archives it after completion, so callers must continue sending full chat history in `messages`. The app-server process uses an isolated `CODEX_HOME` at `~/.codex-api-proxy/codex-home` by default. `codex-api-proxy` symlinks only the current Codex `auth.json` into that isolated home, so the app-server worker can reuse the existing login while not seeing the current user's `config.toml`, MCP config, or plugins. The app-server process is also started with `--disable apps`, `--disable plugins`, `--disable skill_mcp_dependency_install`, and `-c mcp_servers={}`. To keep skills out of the model-visible prompt, `codex-api-proxy` generates a `skills.config=[{name=...,enabled=false}]` override for known system skills and locally discovered skill names. Each request uses an empty `dynamicTools` list, empty `environments`, `approvalPolicy: never`, `sandbox: read-only`, and `ephemeral: true` by default.
45
+ The experimental `app-server` engine uses Codex's long-lived app-server protocol to reduce process startup latency and stream assistant deltas. Each API request starts a fresh Codex thread and archives it after completion, so callers must continue sending full chat history in `messages`. The app-server process uses an isolated `CODEX_HOME` at `~/.codex-api-proxy/codex-home` by default. `codex-api-proxy` symlinks only the current Codex `auth.json` into that isolated home, so the app-server worker can reuse the existing login while not seeing the current user's `config.toml`, MCP config, or plugins. The app-server process is also started with `--disable apps`, `--disable plugins`, `--disable skill_mcp_dependency_install`, and `-c mcp_servers={}`. To keep skills out of the model-visible prompt, `codex-api-proxy` generates a `skills.config=[{name=...,enabled=false}]` override for known system skills and locally discovered skill names. Each request uses `approvalPolicy: never`, `sandbox: read-only`, empty `dynamicTools`, empty `environments`, and `ephemeral: true` by default. With `--agent`, app-server requests use `sandbox: workspace-write` and omit the empty tool/environment overrides so Codex can use its normal agent tools.
45
46
 
46
47
  ## Install
47
48
 
@@ -155,6 +156,7 @@ CLI options:
155
156
  - `--app-server-codex-home`: isolated `CODEX_HOME` used by `app-server` workers, default `~/.codex-api-proxy/codex-home`
156
157
  - `--codex-config`: Codex config override passed as `-c key=value`, repeatable
157
158
  - `--ephemeral`: run `codex exec` with `--ephemeral`, enabled by default
159
+ - `--agent` / `--no-agent`: enable or disable Codex agent tools and workspace writes, default disable
158
160
  - `--fast`: use fast defaults: `--codex-config model_reasoning_effort="low"`
159
161
  - `--default-cwd`: default Codex working directory, default `~/.codex-api-proxy/workspace`
160
162
  - `--allowed-root`: allowed cwd root, repeatable, default `--default-cwd`
@@ -182,6 +184,7 @@ Environment variables are also supported when running the FastAPI app directly:
182
184
  - `CODEX_PROXY_APP_SERVER_CODEX_HOME`: isolated `CODEX_HOME` used by `app-server` workers
183
185
  - `CODEX_PROXY_CODEX_CONFIGS`: `;;`-separated Codex config overrides passed as repeated `-c`
184
186
  - `CODEX_PROXY_EPHEMERAL`: set to `1`, `true`, or `yes` to run `codex exec` with `--ephemeral`; defaults to `true`
187
+ - `CODEX_PROXY_AGENT`: set to `1`, `true`, or `yes` to enable Codex agent tools and workspace writes; defaults to `false`
185
188
  - `CODEX_PROXY_DEFAULT_CWD`: default Codex working directory, default current directory
186
189
  - `CODEX_PROXY_ALLOWED_ROOTS`: colon-separated allowed cwd roots, default `CODEX_PROXY_DEFAULT_CWD`
187
190
  - `CODEX_PROXY_TIMEOUT_SECONDS`: per-request timeout, default `300`
@@ -15,8 +15,9 @@ If you start with `--host 0.0.0.0` or another non-loopback bind address without
15
15
  With the default `exec` engine, Codex subprocesses are launched with `--ignore-user-config` and `--ignore-rules`. This prevents proxy requests from loading user Codex config, MCP servers, plugins, skills, and rule files.
16
16
 
17
17
  Codex subprocesses also use `--sandbox read-only` and `--ephemeral` by default. This keeps calls closer to one-shot model calls where the caller owns conversation context.
18
+ Use `--agent` only for trusted clients when you want Codex to use agent tools and create or modify files under the selected workspace.
18
19
 
19
- The experimental `app-server` engine uses Codex's long-lived app-server protocol to reduce process startup latency and stream assistant deltas. Each API request starts a fresh Codex thread and archives it after completion, so callers must continue sending full chat history in `messages`. The app-server process uses an isolated `CODEX_HOME` at `~/.codex-api-proxy/codex-home` by default. `codex-api-proxy` symlinks only the current Codex `auth.json` into that isolated home, so the app-server worker can reuse the existing login while not seeing the current user's `config.toml`, MCP config, or plugins. The app-server process is also started with `--disable apps`, `--disable plugins`, `--disable skill_mcp_dependency_install`, and `-c mcp_servers={}`. To keep skills out of the model-visible prompt, `codex-api-proxy` generates a `skills.config=[{name=...,enabled=false}]` override for known system skills and locally discovered skill names. Each request uses an empty `dynamicTools` list, empty `environments`, `approvalPolicy: never`, `sandbox: read-only`, and `ephemeral: true` by default.
20
+ The experimental `app-server` engine uses Codex's long-lived app-server protocol to reduce process startup latency and stream assistant deltas. Each API request starts a fresh Codex thread and archives it after completion, so callers must continue sending full chat history in `messages`. The app-server process uses an isolated `CODEX_HOME` at `~/.codex-api-proxy/codex-home` by default. `codex-api-proxy` symlinks only the current Codex `auth.json` into that isolated home, so the app-server worker can reuse the existing login while not seeing the current user's `config.toml`, MCP config, or plugins. The app-server process is also started with `--disable apps`, `--disable plugins`, `--disable skill_mcp_dependency_install`, and `-c mcp_servers={}`. To keep skills out of the model-visible prompt, `codex-api-proxy` generates a `skills.config=[{name=...,enabled=false}]` override for known system skills and locally discovered skill names. Each request uses `approvalPolicy: never`, `sandbox: read-only`, empty `dynamicTools`, empty `environments`, and `ephemeral: true` by default. With `--agent`, app-server requests use `sandbox: workspace-write` and omit the empty tool/environment overrides so Codex can use its normal agent tools.
20
21
 
21
22
  ## Install
22
23
 
@@ -130,6 +131,7 @@ CLI options:
130
131
  - `--app-server-codex-home`: isolated `CODEX_HOME` used by `app-server` workers, default `~/.codex-api-proxy/codex-home`
131
132
  - `--codex-config`: Codex config override passed as `-c key=value`, repeatable
132
133
  - `--ephemeral`: run `codex exec` with `--ephemeral`, enabled by default
134
+ - `--agent` / `--no-agent`: enable or disable Codex agent tools and workspace writes, default disable
133
135
  - `--fast`: use fast defaults: `--codex-config model_reasoning_effort="low"`
134
136
  - `--default-cwd`: default Codex working directory, default `~/.codex-api-proxy/workspace`
135
137
  - `--allowed-root`: allowed cwd root, repeatable, default `--default-cwd`
@@ -157,6 +159,7 @@ Environment variables are also supported when running the FastAPI app directly:
157
159
  - `CODEX_PROXY_APP_SERVER_CODEX_HOME`: isolated `CODEX_HOME` used by `app-server` workers
158
160
  - `CODEX_PROXY_CODEX_CONFIGS`: `;;`-separated Codex config overrides passed as repeated `-c`
159
161
  - `CODEX_PROXY_EPHEMERAL`: set to `1`, `true`, or `yes` to run `codex exec` with `--ephemeral`; defaults to `true`
162
+ - `CODEX_PROXY_AGENT`: set to `1`, `true`, or `yes` to enable Codex agent tools and workspace writes; defaults to `false`
160
163
  - `CODEX_PROXY_DEFAULT_CWD`: default Codex working directory, default current directory
161
164
  - `CODEX_PROXY_ALLOWED_ROOTS`: colon-separated allowed cwd roots, default `CODEX_PROXY_DEFAULT_CWD`
162
165
  - `CODEX_PROXY_TIMEOUT_SECONDS`: per-request timeout, default `300`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codex-api-proxy"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "Local OpenAI-compatible HTTP proxy backed by Codex CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """OpenAI-compatible HTTP proxy backed by the local Codex CLI."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.1"
@@ -362,6 +362,7 @@ async def stream_app_server_turn(
362
362
  model: str | None,
363
363
  codex_configs: list[str],
364
364
  ephemeral: bool,
365
+ agent_enabled: bool = False,
365
366
  timeout_seconds: float,
366
367
  latency_callback: LatencyCallback | None = None,
367
368
  ) -> AsyncIterator[str]:
@@ -372,12 +373,13 @@ async def stream_app_server_turn(
372
373
  "approvalPolicy": "never",
373
374
  "config": config,
374
375
  "cwd": str(cwd),
375
- "dynamicTools": [],
376
- "environments": [],
377
376
  "ephemeral": ephemeral,
378
377
  "model": model,
379
- "sandbox": "read-only",
378
+ "sandbox": "workspace-write" if agent_enabled else "read-only",
380
379
  }
380
+ if not agent_enabled:
381
+ thread_params["dynamicTools"] = []
382
+ thread_params["environments"] = []
381
383
  phase_started_at = time.perf_counter()
382
384
  start_response = await asyncio.wait_for(client.request("thread/start", thread_params), timeout=timeout_seconds)
383
385
  _record_latency(latency_callback, "app_server_thread_start", phase_started_at)
@@ -482,6 +484,7 @@ class AppServerWorkerPool:
482
484
  model: str | None,
483
485
  codex_configs: list[str],
484
486
  ephemeral: bool,
487
+ agent_enabled: bool = False,
485
488
  timeout_seconds: float,
486
489
  latency_callback: LatencyCallback | None = None,
487
490
  ) -> AsyncIterator[str]:
@@ -495,6 +498,7 @@ class AppServerWorkerPool:
495
498
  model=model,
496
499
  codex_configs=codex_configs,
497
500
  ephemeral=ephemeral,
501
+ agent_enabled=agent_enabled,
498
502
  timeout_seconds=timeout_seconds,
499
503
  latency_callback=latency_callback,
500
504
  ):
@@ -92,6 +92,13 @@ def add_settings_args(parser: argparse.ArgumentParser, *, defaults: bool = True)
92
92
  help="Codex config override passed as -c key=value. May be repeated.",
93
93
  )
94
94
  parser.add_argument("--ephemeral", action="store_true", default=True, help="Run codex exec with --ephemeral.")
95
+ parser.add_argument(
96
+ "--agent",
97
+ dest="agent_enabled",
98
+ action=argparse.BooleanOptionalAction,
99
+ default=False if defaults else None,
100
+ help="Enable Codex agent tools and workspace writes. Defaults to disabled.",
101
+ )
95
102
  parser.add_argument("--fast", action="store_true", help="Use fast defaults: low reasoning config.")
96
103
  parser.add_argument(
97
104
  "--default-cwd",
@@ -202,6 +209,7 @@ def settings_from_args(args: argparse.Namespace) -> Settings:
202
209
  model=model,
203
210
  codex_configs=codex_configs,
204
211
  ephemeral=ephemeral,
212
+ agent_enabled=bool(args.agent_enabled),
205
213
  engine=args.engine,
206
214
  workers=args.workers,
207
215
  max_queue_size=args.max_queue_size,
@@ -230,6 +238,7 @@ def state_from_args(args: argparse.Namespace) -> dict[str, object]:
230
238
  "model": settings.model,
231
239
  "codex_configs": settings.codex_configs or [],
232
240
  "ephemeral": settings.ephemeral,
241
+ "agent_enabled": settings.agent_enabled,
233
242
  "engine": settings.engine,
234
243
  "workers": settings.workers,
235
244
  "max_queue_size": settings.max_queue_size,
@@ -294,6 +303,7 @@ def print_start_summary(args: argparse.Namespace, *, pid: int) -> None:
294
303
  print(f" app_server_codex_home: {settings.app_server_codex_home or '<default>'}")
295
304
  print(f" codex_configs: {', '.join(settings.codex_configs or []) if settings.codex_configs else '<none>'}")
296
305
  print(f" ephemeral: {settings.ephemeral}")
306
+ print(f" agent_enabled: {settings.agent_enabled}")
297
307
  print(f" default_cwd: {settings.default_cwd}")
298
308
  print(f" allowed_roots: {', '.join(str(root) for root in allowed_roots)}")
299
309
  print(f" timeout_seconds: {settings.request_timeout_seconds}")
@@ -336,6 +346,11 @@ def args_from_state(state: dict[str, object], overrides: argparse.Namespace) ->
336
346
  )
337
347
  codex_configs = overrides.codex_configs if overrides.codex_configs is not None else state.get("codex_configs", [])
338
348
  ephemeral = overrides.ephemeral or bool(state.get("ephemeral", False))
349
+ agent_enabled = (
350
+ overrides.agent_enabled
351
+ if overrides.agent_enabled is not None
352
+ else bool(state.get("agent_enabled", state.get("workspace_write", False)))
353
+ )
339
354
  if overrides.fast:
340
355
  if overrides.model is None:
341
356
  model = FAST_MODEL
@@ -357,6 +372,7 @@ def args_from_state(state: dict[str, object], overrides: argparse.Namespace) ->
357
372
  app_server_codex_home=app_server_codex_home,
358
373
  codex_configs=list(codex_configs or []),
359
374
  ephemeral=ephemeral,
375
+ agent_enabled=agent_enabled,
360
376
  default_cwd=default_cwd,
361
377
  allowed_roots=allowed_roots or None,
362
378
  timeout_seconds=(
@@ -451,6 +467,8 @@ def start(args: argparse.Namespace) -> int:
451
467
  command.extend(["--codex-config", config])
452
468
  if args.ephemeral:
453
469
  command.append("--ephemeral")
470
+ if args.agent_enabled:
471
+ command.append("--agent")
454
472
  if args.fast:
455
473
  command.append("--fast")
456
474
  if args.api_key:
@@ -532,6 +550,7 @@ def status(args: argparse.Namespace) -> int:
532
550
  print(f" proxy: {state.get('proxy') or '<none>'}")
533
551
  print(f" model: {state.get('model') or '<default>'}")
534
552
  print(f" engine: {state.get('engine', 'exec')}")
553
+ print(f" agent_enabled: {state.get('agent_enabled', state.get('workspace_write', False))}")
535
554
  print(f" workers: {state.get('workers', 1)}")
536
555
  print(f" max_queue_size: {state.get('max_queue_size', 64)}")
537
556
  print(f" queue_timeout_seconds: {state.get('queue_timeout_seconds', 30)}")
@@ -131,6 +131,7 @@ def build_codex_command(
131
131
  model: str | None,
132
132
  codex_configs: list[str],
133
133
  ephemeral: bool,
134
+ agent_enabled: bool = False,
134
135
  ) -> list[str]:
135
136
  has_sandbox_override = any(config.strip().startswith("sandbox_mode") for config in codex_configs)
136
137
  command = [
@@ -142,7 +143,7 @@ def build_codex_command(
142
143
  "--ignore-rules",
143
144
  ]
144
145
  if not has_sandbox_override:
145
- command.extend(["--sandbox", "read-only"])
146
+ command.extend(["--sandbox", "workspace-write" if agent_enabled else "read-only"])
146
147
  command.extend(["--cd", str(cwd)])
147
148
  if model:
148
149
  command.extend(["--model", model])
@@ -231,6 +232,7 @@ async def run_codex_exec(
231
232
  model: str | None = None,
232
233
  codex_configs: list[str] | None = None,
233
234
  ephemeral: bool = False,
235
+ agent_enabled: bool = False,
234
236
  latency_callback: Callable[[str, float], None] | None = None,
235
237
  ) -> str:
236
238
  command_started_at = time.perf_counter()
@@ -240,6 +242,7 @@ async def run_codex_exec(
240
242
  model=model,
241
243
  codex_configs=codex_configs or [],
242
244
  ephemeral=ephemeral,
245
+ agent_enabled=agent_enabled,
243
246
  )
244
247
  if latency_callback:
245
248
  latency_callback("codex_command_build", (time.perf_counter() - command_started_at) * 1000)
@@ -25,6 +25,7 @@ class Settings:
25
25
  model: str | None = None
26
26
  codex_configs: list[str] | None = None
27
27
  ephemeral: bool = True
28
+ agent_enabled: bool = False
28
29
  engine: str = "exec"
29
30
  workers: int = 1
30
31
  max_queue_size: int = 64
@@ -55,6 +56,7 @@ class Settings:
55
56
  if item
56
57
  ],
57
58
  ephemeral=os.environ.get("CODEX_PROXY_EPHEMERAL", "true").lower() in {"1", "true", "yes"},
59
+ agent_enabled=os.environ.get("CODEX_PROXY_AGENT", "false").lower() in {"1", "true", "yes"},
58
60
  engine=os.environ.get("CODEX_PROXY_ENGINE", "exec"),
59
61
  workers=int(os.environ.get("CODEX_PROXY_WORKERS", "1")),
60
62
  max_queue_size=int(os.environ.get("CODEX_PROXY_MAX_QUEUE_SIZE", "64")),
@@ -322,6 +322,7 @@ def create_app(
322
322
  "model": active_settings.model if request.model == "codex-local" else request.model,
323
323
  "codex_configs": active_settings.codex_configs or [],
324
324
  "ephemeral": active_settings.ephemeral,
325
+ "agent_enabled": active_settings.agent_enabled,
325
326
  "timeout_seconds": active_settings.request_timeout_seconds,
326
327
  "latency_callback": record_codex_phase,
327
328
  }
@@ -436,6 +437,7 @@ def create_app(
436
437
  model=active_settings.model,
437
438
  codex_configs=active_settings.codex_configs or [],
438
439
  ephemeral=active_settings.ephemeral,
440
+ agent_enabled=active_settings.agent_enabled,
439
441
  latency_callback=record_codex_phase,
440
442
  )
441
443
  phases_ms["codex_exec"] = _elapsed_ms(codex_started_at)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-api-proxy
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Local OpenAI-compatible HTTP proxy backed by Codex CLI
5
5
  Author: codex-api-proxy contributors
6
6
  License-Expression: MIT
@@ -40,8 +40,9 @@ If you start with `--host 0.0.0.0` or another non-loopback bind address without
40
40
  With the default `exec` engine, Codex subprocesses are launched with `--ignore-user-config` and `--ignore-rules`. This prevents proxy requests from loading user Codex config, MCP servers, plugins, skills, and rule files.
41
41
 
42
42
  Codex subprocesses also use `--sandbox read-only` and `--ephemeral` by default. This keeps calls closer to one-shot model calls where the caller owns conversation context.
43
+ Use `--agent` only for trusted clients when you want Codex to use agent tools and create or modify files under the selected workspace.
43
44
 
44
- The experimental `app-server` engine uses Codex's long-lived app-server protocol to reduce process startup latency and stream assistant deltas. Each API request starts a fresh Codex thread and archives it after completion, so callers must continue sending full chat history in `messages`. The app-server process uses an isolated `CODEX_HOME` at `~/.codex-api-proxy/codex-home` by default. `codex-api-proxy` symlinks only the current Codex `auth.json` into that isolated home, so the app-server worker can reuse the existing login while not seeing the current user's `config.toml`, MCP config, or plugins. The app-server process is also started with `--disable apps`, `--disable plugins`, `--disable skill_mcp_dependency_install`, and `-c mcp_servers={}`. To keep skills out of the model-visible prompt, `codex-api-proxy` generates a `skills.config=[{name=...,enabled=false}]` override for known system skills and locally discovered skill names. Each request uses an empty `dynamicTools` list, empty `environments`, `approvalPolicy: never`, `sandbox: read-only`, and `ephemeral: true` by default.
45
+ The experimental `app-server` engine uses Codex's long-lived app-server protocol to reduce process startup latency and stream assistant deltas. Each API request starts a fresh Codex thread and archives it after completion, so callers must continue sending full chat history in `messages`. The app-server process uses an isolated `CODEX_HOME` at `~/.codex-api-proxy/codex-home` by default. `codex-api-proxy` symlinks only the current Codex `auth.json` into that isolated home, so the app-server worker can reuse the existing login while not seeing the current user's `config.toml`, MCP config, or plugins. The app-server process is also started with `--disable apps`, `--disable plugins`, `--disable skill_mcp_dependency_install`, and `-c mcp_servers={}`. To keep skills out of the model-visible prompt, `codex-api-proxy` generates a `skills.config=[{name=...,enabled=false}]` override for known system skills and locally discovered skill names. Each request uses `approvalPolicy: never`, `sandbox: read-only`, empty `dynamicTools`, empty `environments`, and `ephemeral: true` by default. With `--agent`, app-server requests use `sandbox: workspace-write` and omit the empty tool/environment overrides so Codex can use its normal agent tools.
45
46
 
46
47
  ## Install
47
48
 
@@ -155,6 +156,7 @@ CLI options:
155
156
  - `--app-server-codex-home`: isolated `CODEX_HOME` used by `app-server` workers, default `~/.codex-api-proxy/codex-home`
156
157
  - `--codex-config`: Codex config override passed as `-c key=value`, repeatable
157
158
  - `--ephemeral`: run `codex exec` with `--ephemeral`, enabled by default
159
+ - `--agent` / `--no-agent`: enable or disable Codex agent tools and workspace writes, default disable
158
160
  - `--fast`: use fast defaults: `--codex-config model_reasoning_effort="low"`
159
161
  - `--default-cwd`: default Codex working directory, default `~/.codex-api-proxy/workspace`
160
162
  - `--allowed-root`: allowed cwd root, repeatable, default `--default-cwd`
@@ -182,6 +184,7 @@ Environment variables are also supported when running the FastAPI app directly:
182
184
  - `CODEX_PROXY_APP_SERVER_CODEX_HOME`: isolated `CODEX_HOME` used by `app-server` workers
183
185
  - `CODEX_PROXY_CODEX_CONFIGS`: `;;`-separated Codex config overrides passed as repeated `-c`
184
186
  - `CODEX_PROXY_EPHEMERAL`: set to `1`, `true`, or `yes` to run `codex exec` with `--ephemeral`; defaults to `true`
187
+ - `CODEX_PROXY_AGENT`: set to `1`, `true`, or `yes` to enable Codex agent tools and workspace writes; defaults to `false`
185
188
  - `CODEX_PROXY_DEFAULT_CWD`: default Codex working directory, default current directory
186
189
  - `CODEX_PROXY_ALLOWED_ROOTS`: colon-separated allowed cwd roots, default `CODEX_PROXY_DEFAULT_CWD`
187
190
  - `CODEX_PROXY_TIMEOUT_SECONDS`: per-request timeout, default `300`
@@ -247,6 +247,7 @@ def test_chat_completion_passes_proxy_to_runner(tmp_path: Path) -> None:
247
247
  assert kwargs["model"] == "gpt-5-mini"
248
248
  assert kwargs["codex_configs"] == ['model_reasoning_effort="low"']
249
249
  assert kwargs["ephemeral"] is True
250
+ assert kwargs["agent_enabled"] is True
250
251
  return "hello"
251
252
 
252
253
  app = create_app(
@@ -257,6 +258,7 @@ def test_chat_completion_passes_proxy_to_runner(tmp_path: Path) -> None:
257
258
  model="gpt-5-mini",
258
259
  codex_configs=['model_reasoning_effort="low"'],
259
260
  ephemeral=True,
261
+ agent_enabled=True,
260
262
  ),
261
263
  runner=fake_runner,
262
264
  )
@@ -278,11 +280,18 @@ def test_chat_completion_uses_app_server_engine(tmp_path: Path) -> None:
278
280
  assert kwargs["cwd"] == tmp_path.resolve()
279
281
  assert kwargs["prompt"] == "[user]\nhi\n"
280
282
  assert kwargs["model"] == "gpt-5-mini"
283
+ assert kwargs["agent_enabled"] is True
281
284
  yield "he"
282
285
  yield "llo"
283
286
 
284
287
  app = create_app(
285
- Settings(default_cwd=tmp_path, allowed_roots=[tmp_path], engine="app-server", model="gpt-5-mini"),
288
+ Settings(
289
+ default_cwd=tmp_path,
290
+ allowed_roots=[tmp_path],
291
+ engine="app-server",
292
+ model="gpt-5-mini",
293
+ agent_enabled=True,
294
+ ),
286
295
  runner=fake_exec_runner,
287
296
  app_server_runner=fake_app_server_runner,
288
297
  )
@@ -72,6 +72,7 @@ async def test_stream_app_server_turn_creates_fresh_thread_and_streams_deltas(tm
72
72
  model="gpt-5-mini",
73
73
  codex_configs=['model_reasoning_effort="low"'],
74
74
  ephemeral=True,
75
+ agent_enabled=False,
75
76
  timeout_seconds=30,
76
77
  latency_callback=lambda name, elapsed: phases.append((name, elapsed)),
77
78
  )
@@ -124,12 +125,39 @@ async def test_app_server_pool_rejects_when_queue_is_full() -> None:
124
125
  model=None,
125
126
  codex_configs=[],
126
127
  ephemeral=True,
128
+ agent_enabled=False,
127
129
  timeout_seconds=1,
128
130
  latency_callback=None,
129
131
  ):
130
132
  pass
131
133
 
132
134
 
135
+ @pytest.mark.asyncio
136
+ async def test_stream_app_server_turn_enables_tools_and_workspace_write_when_agent_enabled(tmp_path: Path) -> None:
137
+ client = FakeClient()
138
+
139
+ chunks = [
140
+ chunk
141
+ async for chunk in stream_app_server_turn(
142
+ client=client,
143
+ cwd=tmp_path,
144
+ prompt="[user]\nhi",
145
+ model=None,
146
+ codex_configs=[],
147
+ ephemeral=True,
148
+ agent_enabled=True,
149
+ timeout_seconds=30,
150
+ latency_callback=None,
151
+ )
152
+ ]
153
+
154
+ assert chunks == ["he", "llo"]
155
+ thread_start_params = client.requests[0][1]
156
+ assert thread_start_params["sandbox"] == "workspace-write"
157
+ assert "dynamicTools" not in thread_start_params
158
+ assert "environments" not in thread_start_params
159
+
160
+
133
161
  def test_prepare_isolated_codex_home_reuses_only_auth_file(tmp_path: Path) -> None:
134
162
  source_home = tmp_path / "source-codex"
135
163
  isolated_home = tmp_path / "isolated-codex"
@@ -108,16 +108,32 @@ def test_start_accepts_overrides(tmp_path: Path) -> None:
108
108
  assert settings.max_concurrency == 3
109
109
  assert settings.proxy == "http://127.0.0.1:7890"
110
110
  assert settings.engine == "exec"
111
+ assert settings.agent_enabled is False
111
112
  assert settings.workers == 1
112
113
  assert settings.max_queue_size == 64
113
114
  assert settings.queue_timeout_seconds == 30
114
115
  assert settings.app_server_codex_home == default_app_server_codex_home()
115
116
 
116
117
 
118
+ def test_start_accepts_agent_switch(tmp_path: Path) -> None:
119
+ parser = build_parser()
120
+ args = parser.parse_args(["start", "--default-cwd", str(tmp_path), "--agent"])
121
+
122
+ settings = settings_from_args(args)
123
+
124
+ assert settings.agent_enabled is True
125
+
126
+
117
127
  def test_start_rejects_removed_direct_options(tmp_path: Path) -> None:
118
128
  parser = build_parser()
119
129
 
120
- for option in ("--backend", "--no-direct-fallback", "--codex-auth-file", "--codex-config-file"):
130
+ for option in (
131
+ "--backend",
132
+ "--no-direct-fallback",
133
+ "--codex-auth-file",
134
+ "--codex-config-file",
135
+ "--workspace-write",
136
+ ):
121
137
  with pytest.raises(SystemExit):
122
138
  parser.parse_args(["start", option, str(tmp_path / "value")])
123
139
 
@@ -231,6 +247,7 @@ def test_start_state_round_trip_restricts_permissions(tmp_path: Path) -> None:
231
247
  "9999",
232
248
  "--proxy",
233
249
  "http://127.0.0.1:8118",
250
+ "--agent",
234
251
  "--default-cwd",
235
252
  str(root),
236
253
  "--allowed-root",
@@ -253,6 +270,7 @@ def test_start_state_round_trip_restricts_permissions(tmp_path: Path) -> None:
253
270
  assert loaded["model"] is None
254
271
  assert loaded["codex_configs"] == []
255
272
  assert loaded["ephemeral"] is True
273
+ assert loaded["agent_enabled"] is True
256
274
  assert loaded["engine"] == "exec"
257
275
  assert loaded["workers"] == 1
258
276
  assert loaded["max_queue_size"] == 64
@@ -314,6 +332,7 @@ def test_restart_reuses_state_and_allows_overrides(tmp_path: Path, monkeypatch)
314
332
  "8765",
315
333
  "--proxy",
316
334
  "http://127.0.0.1:8118",
335
+ "--agent",
317
336
  "--default-cwd",
318
337
  str(root),
319
338
  "--pid-file",
@@ -334,7 +353,18 @@ def test_restart_reuses_state_and_allows_overrides(tmp_path: Path, monkeypatch)
334
353
 
335
354
  def fake_start(args):
336
355
  assert args.fast is False
337
- calls.append(("start", args.host, args.port, args.proxy, args.default_cwd, args.pid_file, args.log_file))
356
+ calls.append(
357
+ (
358
+ "start",
359
+ args.host,
360
+ args.port,
361
+ args.proxy,
362
+ args.agent_enabled,
363
+ args.default_cwd,
364
+ args.pid_file,
365
+ args.log_file,
366
+ )
367
+ )
338
368
  return 0
339
369
 
340
370
  monkeypatch.setattr(cli, "stop", fake_stop)
@@ -343,7 +373,7 @@ def test_restart_reuses_state_and_allows_overrides(tmp_path: Path, monkeypatch)
343
373
  assert restart(restart_args) == 0
344
374
  assert calls == [
345
375
  ("stop", pid_file),
346
- ("start", "127.0.0.1", 9999, "http://127.0.0.1:8118", root, pid_file, log_file),
376
+ ("start", "127.0.0.1", 9999, "http://127.0.0.1:8118", True, root, pid_file, log_file),
347
377
  ]
348
378
 
349
379
 
@@ -392,6 +422,7 @@ def test_start_prints_state_file_and_effective_parameters(tmp_path: Path, monkey
392
422
  assert "chat_completions_url: http://127.0.0.1:9999/v1/chat/completions" in output
393
423
  assert "proxy: http://127.0.0.1:8118" in output
394
424
  assert "engine: exec" in output
425
+ assert "agent_enabled: False" in output
395
426
  assert "workers: 1" in output
396
427
  assert "max_queue_size: 64" in output
397
428
  assert "queue_timeout_seconds: 30" in output
@@ -485,6 +516,7 @@ def test_status_verbose_prints_runtime_details_from_state(tmp_path: Path, monkey
485
516
  "9999",
486
517
  "--proxy",
487
518
  "http://127.0.0.1:8118",
519
+ "--agent",
488
520
  "--pid-file",
489
521
  str(pid_file),
490
522
  "--log-file",
@@ -502,6 +534,7 @@ def test_status_verbose_prints_runtime_details_from_state(tmp_path: Path, monkey
502
534
  output = capsys.readouterr().out
503
535
 
504
536
  assert "engine: app-server" in output
537
+ assert "agent_enabled: True" in output
505
538
  assert "workers: 2" in output
506
539
  assert "proxy: http://127.0.0.1:8118" in output
507
540
  assert f"log: {log_file}" in output
@@ -120,6 +120,7 @@ def test_build_codex_command_omits_optional_flags(tmp_path) -> None:
120
120
  model=None,
121
121
  codex_configs=[],
122
122
  ephemeral=False,
123
+ agent_enabled=False,
123
124
  )
124
125
 
125
126
  assert "--model" not in command
@@ -131,18 +132,19 @@ def test_build_codex_command_omits_optional_flags(tmp_path) -> None:
131
132
  assert "read-only" in command
132
133
 
133
134
 
134
- def test_build_codex_command_does_not_add_read_only_sandbox_when_config_overrides(tmp_path) -> None:
135
+ def test_build_codex_command_uses_workspace_write_sandbox_when_agent_enabled(tmp_path) -> None:
135
136
  command = build_codex_command(
136
137
  codex_bin="codex",
137
138
  cwd=tmp_path,
138
139
  model=None,
139
- codex_configs=['sandbox_mode="workspace-write"'],
140
+ codex_configs=[],
140
141
  ephemeral=True,
142
+ agent_enabled=True,
141
143
  )
142
144
 
143
- assert "--sandbox" not in command
144
- assert "-c" in command
145
- assert 'sandbox_mode="workspace-write"' in command
145
+ assert "--sandbox" in command
146
+ sandbox_index = command.index("--sandbox")
147
+ assert command[sandbox_index + 1] == "workspace-write"
146
148
 
147
149
 
148
150
  def test_summarize_process_error_prefers_stdout_error_when_stderr_empty() -> None:
@@ -34,6 +34,7 @@ def test_child_cwd_is_allowed(tmp_path: Path) -> None:
34
34
  def test_from_env_reads_engine_worker_and_queue_settings(tmp_path: Path, monkeypatch) -> None:
35
35
  monkeypatch.setenv("CODEX_PROXY_DEFAULT_CWD", str(tmp_path))
36
36
  monkeypatch.setenv("CODEX_PROXY_ENGINE", "app-server")
37
+ monkeypatch.setenv("CODEX_PROXY_AGENT", "true")
37
38
  monkeypatch.setenv("CODEX_PROXY_WORKERS", "4")
38
39
  monkeypatch.setenv("CODEX_PROXY_MAX_QUEUE_SIZE", "8")
39
40
  monkeypatch.setenv("CODEX_PROXY_QUEUE_TIMEOUT_SECONDS", "2.5")
@@ -43,6 +44,7 @@ def test_from_env_reads_engine_worker_and_queue_settings(tmp_path: Path, monkeyp
43
44
  settings = Settings.from_env()
44
45
 
45
46
  assert settings.engine == "app-server"
47
+ assert settings.agent_enabled is True
46
48
  assert settings.workers == 4
47
49
  assert settings.max_queue_size == 8
48
50
  assert settings.queue_timeout_seconds == 2.5