Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ATTRIBUTIONS-Python.md
Original file line number Diff line number Diff line change
Expand Up @@ -5887,7 +5887,7 @@ Apache License
limitations under the License.
```

## openai-codex (0.1.0b3)
## openai-codex (0.144.4)

### Licenses
License: `Apache-2.0`
Expand All @@ -5897,7 +5897,7 @@ License: `Apache-2.0`
(No license file found in locked artifact for openai-codex; see package metadata or PyPI.)
```

## openai-codex-cli-bin (0.137.0a4)
## openai-codex-cli-bin (0.144.4)

### Licenses
License: `Apache-2.0`
Expand Down
26 changes: 19 additions & 7 deletions adapters/codex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,12 @@ modify a user's Codex login. Set the endpoint in
`models.default.settings.base_url` or `NVIDIA_FRONTIER_BASE_URL`; the adapter
does not assume a default frontier endpoint.

The dependency graph includes `openai-codex-cli-bin`. The Codex SDK owns this
pinned app-server distribution; Fabric does not treat it as a user-installed
command or an adapter descriptor requirement.
The adapter depends on the Codex SDK, which installs and selects its matching
app-server runtime. Fabric does not declare the runtime package directly or
treat it as a user-installed command or adapter descriptor requirement.

This adapter pins `openai-codex==0.1.0b3`, which pins
`openai-codex-cli-bin==0.137.0a4`. A newer `codex` command on `PATH` is not used
implicitly. When testing a newer compatible runtime, set
A `codex` command on `PATH` is not selected implicitly. To override the
SDK-selected runtime intentionally, set
`harness.settings.codex_bin` to an app-server path that is absolute or relative
to the explicit `base_dir`. Fabric passes the resolved path through
`CodexConfig.codex_bin`; the SDK remains the execution driver.
Expand All @@ -88,8 +87,20 @@ Use normalized `FabricConfig` fields for portable configuration:
provider and NVIDIA-hosted Responses-compatible models through the `nvidia`
provider.
- `environment.workspace` sets the working directory.
- `mcp` maps stdio, HTTP, and streamable HTTP servers into request-scoped Codex
`mcp_servers` configuration. For stdio, Fabric parses `url` as a command plus
arguments.
- `skills.paths` names skill directories that contain `SKILL.md`. The adapter
registers each directory as a process-scoped Codex skill root so Codex can
select matching skills through its normal discovery behavior.
- `telemetry` enables native OpenTelemetry or NeMo Relay observability.

The Codex adapter does not declare `tools.blocked` support. The current Codex
runtime has per-MCP-server tool filters, but it does not provide one complete
deny boundary for built-in, local, MCP, and hosted tools. Fabric therefore
routes normalized blocked-tool policy as unsupported instead of applying a
partial policy.

Codex-specific controls belong in `harness.settings`:

- `sandbox`: `read-only`, `workspace-write`, or `danger-full-access`
Expand All @@ -98,7 +109,8 @@ Codex-specific controls belong in `harness.settings`:
- `personality`, `reasoning_effort`, `service_name`, and `service_tier`
- `output_schema` for SDK-native structured output
- `codex_bin` for an explicit Codex app-server runtime override
- `config_overrides` as dotted request-scoped Codex configuration keys
- `config_overrides` as dotted request-scoped Codex configuration keys, such as
Codex-only MCP timeout or required-server options
- `timeout_seconds`, defaulting to 1800
- `env` for variables explicitly forwarded to the Codex runtime
- `nemo_relay_command` for the optional external Relay gateway executable
Expand Down
2 changes: 1 addition & 1 deletion adapters/codex/fabric-adapter.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"callable": "run"
},
"config": {
"accepts": ["models", "telemetry"]
"accepts": ["models", "mcp", "skills", "telemetry"]
},
"telemetry": {
"providers": {
Expand Down
7 changes: 1 addition & 6 deletions adapters/codex/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"nemo-fabric-adapters-common == 0.1.0",
"openai-codex==0.1.0b3",
"openai-codex==0.144.4",
"tomli-w~=1.2",
]

Expand All @@ -43,8 +43,3 @@ include = ["nemo_fabric_adapters.codex*"]

[tool.uv.sources]
nemo-fabric-adapters-common = { path = "../common", editable = true }

[tool.uv]
# The SDK pins this prerelease runtime. Constraining that transitive package
# lets uv resolve it without enabling prereleases for unrelated dependencies.
constraint-dependencies = ["openai-codex-cli-bin==0.137.0a4"]
133 changes: 132 additions & 1 deletion adapters/codex/src/nemo_fabric_adapters/codex/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import json
import math
import os
import shlex
from dataclasses import asdict, dataclass, is_dataclass
from enum import Enum
from hashlib import sha256
Expand All @@ -25,6 +26,7 @@
TransportClosedError,
is_retryable_error,
)
from openai_codex.generated.v2_all import SkillsExtraRootsSetResponse
from openai_codex.types import Personality, ReasoningEffort, TurnStatus

import nemo_fabric_adapters.common.relay_gateway as relay_gateway
Expand Down Expand Up @@ -83,7 +85,9 @@
}
NORMALIZED_SETTING_FIELDS = {
"cwd": "FabricConfig.environment.workspace",
"mcp_servers": "FabricConfig.mcp",
"model_name": "FabricConfig.models",
"skills": "FabricConfig.skills",
}


Expand Down Expand Up @@ -173,6 +177,126 @@ def request_prompt(payload: dict[str, Any]) -> str:
return value


def _native_capabilities(payload: dict[str, Any]) -> dict[str, Any]:
Comment thread
AjayThorve marked this conversation as resolved.
plan = _mapping(common_utils.capability_plan(payload), name="capability_plan")
return _mapping(plan.get("native"), name="capability_plan.native")


def _native_mcp_servers(payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
servers = _mapping(
_native_capabilities(payload).get("mcp_servers"),
name="native MCP servers",
)
result: dict[str, dict[str, Any]] = {}
for name, raw in sorted(servers.items()):
if not isinstance(name, str) or not name:
raise AdapterConfigError(
"codex_invalid_configuration",
"MCP server names must be non-empty strings",
)
server = _mapping(raw, name=f"MCP server {name}")
transport = server.get("transport")
if not isinstance(transport, str) or not transport:
raise AdapterConfigError(
"codex_invalid_configuration",
f"MCP server {name} transport is required",
)
target = server.get("url")
if not isinstance(target, str) or not target:
raise AdapterConfigError(
"codex_invalid_configuration",
f"MCP server {name} URL is required",
)
target = os.path.expandvars(target).strip()
if not target:
raise AdapterConfigError(
"codex_invalid_configuration",
f"MCP server {name} URL is required",
)
normalized_transport = transport.strip().lower().replace("_", "-")
if normalized_transport == "stdio":
try:
command = shlex.split(target)
except ValueError as error:
raise AdapterConfigError(
"codex_invalid_configuration",
f"MCP server {name} command is invalid",
) from error
if not command:
raise AdapterConfigError(
"codex_invalid_configuration",
f"MCP server {name} command is required",
)
result[name] = {"command": command[0], "args": command[1:]}
elif normalized_transport in {"http", "streamable-http"}:
result[name] = {"url": target}
else:
raise AdapterConfigError(
"codex_invalid_configuration",
f"unsupported Codex MCP transport: {transport}",
)
return result


def _native_skill_paths(payload: dict[str, Any]) -> list[Path]:
values = _native_capabilities(payload).get("skill_paths", [])
if not isinstance(values, list) or any(
not isinstance(value, (str, Path)) or not str(value) for value in values
):
raise AdapterConfigError(
"codex_invalid_configuration",
"native skill_paths must be a list of paths",
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

paths: list[Path] = []
names: set[str] = set()
config_root = Path(common_utils.base_dir(payload))
for value in values:
skill_path = Path(value)
if not skill_path.is_absolute():
skill_path = config_root / skill_path
skill_path = skill_path.resolve()
skill_file = skill_path / "SKILL.md"
if not skill_path.is_dir() or not skill_file.is_file():
raise AdapterConfigError(
"codex_invalid_configuration",
"Fabric skill path must be a directory containing SKILL.md: "
f"{skill_path}",
)
name = skill_path.name
if not name or name in names:
raise AdapterConfigError(
"codex_invalid_configuration",
f"Fabric skill names must be unique: {name}",
)
names.add(name)
paths.append(skill_path)
return paths


async def _register_skill_roots(codex: AsyncCodex, skill_paths: list[Path]) -> None:
if not skill_paths:
return

# The pinned SDK does not yet wrap the app-server's process-scoped
# skills/extraRoots/set request. Keep the pinned-SDK compatibility seam
# here so arbitrary Fabric skill paths become discoverable without
# modifying the consumer workspace.
await codex.models()
client = getattr(codex, "_client", None)
request = getattr(client, "request", None)
if not callable(request):
raise AdapterConfigError(
"codex_invalid_configuration",
"Codex SDK does not expose the required skill registration request",
)
await request(
"skills/extraRoots/set",
{"extraRoots": [str(path) for path in skill_paths]},
response_model=SkillsExtraRootsSetResponse,
)


def resolve_cwd(payload: dict[str, Any]) -> Path:
environment = _mapping(
common_utils.environment_payload(payload), name="runtime environment"
Expand Down Expand Up @@ -569,6 +693,9 @@ def thread_config(

config = native_codex_telemetry_config(payload)
_merge_config(config, nvidia_model_provider_config(payload))
mcp_servers = _native_mcp_servers(payload)
if mcp_servers:
config["mcp_servers"] = mcp_servers
overrides = _mapping(
_settings(payload).get("config_overrides"),
name="harness.settings.config_overrides",
Expand Down Expand Up @@ -657,6 +784,7 @@ def validate_payload(payload: dict[str, Any]) -> str:
settings = _settings(payload)
_validate_settings_boundary(settings)
request_prompt(payload)
_native_skill_paths(payload)
fabric_runtime_id = runtime_id(payload)
resolve_cwd(payload)
selected_model(payload)
Expand Down Expand Up @@ -822,6 +950,8 @@ async def invoke_codex_sdk(

settings = _settings(payload)
config = thread_config(payload, relay)
skill_paths = _native_skill_paths(payload)
prompt = request_prompt(payload)
client_config = sdk_config(payload, relay)
codex: AsyncCodex | None = None
handle = None
Expand All @@ -836,6 +966,7 @@ async def invoke_codex_sdk(
)
codex = AsyncCodex(config=client_config)
async with asyncio.timeout(timeout_seconds(payload)):
await _register_skill_roots(codex, skill_paths)
common = {
"approval_mode": approval_mode(payload),
"base_instructions": _optional_string(settings, "base_instructions"),
Expand Down Expand Up @@ -864,7 +995,7 @@ async def invoke_codex_sdk(
)
thread_id = thread.id
handle = await thread.turn(
request_prompt(payload),
prompt,
effort=_reasoning_effort(payload),
output_schema=_output_schema(payload),
)
Expand Down
29 changes: 13 additions & 16 deletions adapters/codex/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"callable": "run"
},
"config": {
"accepts": ["models", "telemetry"]
"accepts": ["models", "mcp", "skills", "telemetry"]
},
"telemetry": {
"providers": {
Expand Down
Loading
Loading