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.
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/PKG-INFO +5 -2
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/README.md +4 -1
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/pyproject.toml +1 -1
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/__init__.py +1 -1
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/app_server_runner.py +7 -3
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/cli.py +19 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/codex_runner.py +4 -1
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/config.py +2 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/main.py +2 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/PKG-INFO +5 -2
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_api.py +10 -1
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_app_server_runner.py +28 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_cli.py +36 -3
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_codex_runner.py +7 -5
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_config.py +2 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/setup.cfg +0 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/prompt.py +0 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy/schemas.py +0 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/SOURCES.txt +0 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/dependency_links.txt +0 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/entry_points.txt +0 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/requires.txt +0 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/top_level.txt +0 -0
- {codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/tests/test_prompt.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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`
|
|
@@ -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.
|
|
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
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
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=[
|
|
140
|
+
codex_configs=[],
|
|
140
141
|
ephemeral=True,
|
|
142
|
+
agent_enabled=True,
|
|
141
143
|
)
|
|
142
144
|
|
|
143
|
-
assert "--sandbox"
|
|
144
|
-
|
|
145
|
-
assert
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{codex_api_proxy-0.1.0 → codex_api_proxy-0.1.1}/src/codex_api_proxy.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|