Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion .github/workflows/daily-arxiv-researcher.lock.yml

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

2 changes: 1 addition & 1 deletion .github/workflows/smoke-codex.lock.yml

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

2 changes: 2 additions & 0 deletions docs/src/content/docs/engines/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ Analyze the repository and create a concise daily status report covering:

Codex enables its shared native search/browsing tool when either `tools.web-search` or `tools.web-fetch` is enabled. Search and page fetching cannot be disabled independently; use `network.hosted-web` to restrict hosted retrieval. Codex can disable shell execution completely but cannot enforce a nonempty per-command `tools.bash` allowlist.

With OpenAI inference through the AWF sandbox, enabling `network.hosted-web` also enables Codex's standalone `web.run` search tool for Responses Lite models, which do not receive the hosted `web_search` tool. Without hosted web, this provider flag is not set. Codex standalone search does not support `network.hosted-web.max-uses`; omit that limit when using Codex with OpenAI inference.

Codex does not support bare mode, Copilot-style `max-continuations`, or native `engine.agent` selection. `engine.harness.use` can select a replacement harness that has been provisioned in the setup-action directory before execution. See the [AI engine feature comparison](/gh-aw/reference/engines/#engine-feature-comparison).

When a workflow does not declare any `plugins`, GitHub Agentic Workflows writes [`features.plugins=false`](https://developers.openai.com/codex/config-reference/#features) to Codex's generated `config.toml`. This prevents Codex from contacting the ChatGPT plugin catalog or synchronizing the curated plugin repository at startup. Codex does not provide a narrower setting that disables only startup synchronization, so workflows that declare Agent Plugins keep the plugin subsystem and its startup checks enabled. Directly configured MCP servers are unaffected.
Expand Down
21 changes: 13 additions & 8 deletions pkg/workflow/codex_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,20 @@ func (e *CodexEngine) buildNativeConfig(workflowData *WorkflowData, mcpTools []s
config.Defaults["features"] = features
if isFirewallEnabled(workflowData) {
config.Defaults["model_provider"] = codexOpenAIProxyProviderID
proxy := map[string]any{
"name": codexOpenAIProxyProviderName,
"base_url": e.getOpenAIProxyProviderBaseURL(workflowData),
"env_key": "CODEX_API_KEY",
"wire_api": "responses",
"requires_openai_auth": false,
"supports_websockets": false,
}
if workflowData.NetworkPermissions != nil && workflowData.NetworkPermissions.HostedWeb != nil &&
workflowData.NetworkPermissions.HostedWeb.Enabled && e.ResolveLLMProvider(workflowData) == LLMProviderOpenAI {
proxy["supports_standalone_web_search"] = true
}
config.Defaults["model_providers"] = map[string]any{
codexOpenAIProxyProviderID: map[string]any{
"name": codexOpenAIProxyProviderName,
"base_url": e.getOpenAIProxyProviderBaseURL(workflowData),
"env_key": "CODEX_API_KEY",
"wire_api": "responses",
"requires_openai_auth": false,
"supports_websockets": false,
},
codexOpenAIProxyProviderID: proxy,
}
}
config.Defaults["mcp_servers"] = codexNativeServerDefaults(workflowData, mcpTools)
Expand Down
30 changes: 30 additions & 0 deletions pkg/workflow/codex_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,36 @@ process.stdout.write(serializeConfig(buildConfig({github: {headers: {Authorizati
assert.Equal(t, "Bearer \"quoted\"\\token", headers["Authorization"])
}

func TestCodexStandaloneWebSearchProvider(t *testing.T) {
for _, test := range []struct {
name string
provider LLMProvider
policy *HostedWebPolicy
wantFlag bool
}{
{name: "OpenAI hosted web", provider: LLMProviderOpenAI, policy: &HostedWebPolicy{Enabled: true, Allowed: []string{"docs.example.com"}}, wantFlag: true},
{name: "OpenAI without hosted web", provider: LLMProviderOpenAI},
{name: "OpenAI disabled hosted web", provider: LLMProviderOpenAI, policy: &HostedWebPolicy{}},
{name: "GitHub hosted web", provider: LLMProviderGitHub, policy: &HostedWebPolicy{Enabled: true, Allowed: []string{"docs.example.com"}}},
} {
t.Run(test.name, func(t *testing.T) {
data := &WorkflowData{
EngineConfig: &EngineConfig{ID: "codex", LLMProvider: test.provider},
NetworkPermissions: &NetworkPermissions{Firewall: &FirewallConfig{Enabled: true}, HostedWeb: test.policy},
}
var output strings.Builder
require.NoError(t, renderCodexMCPConfigForTest(t, NewCodexEngine(), &output, map[string]any{}, nil, data))
config := decodeCodexBootstrap(t, output.String())
proxy := config["model_providers"].(map[string]any)[codexOpenAIProxyProviderID].(map[string]any)
if test.wantFlag {
assert.Equal(t, true, proxy["supports_standalone_web_search"])
} else {
assert.NotContains(t, proxy, "supports_standalone_web_search")
}
})
}
}

func TestCodexGitHubInferenceDisablesUnsupportedExecTool(t *testing.T) {
for _, test := range []struct {
name string
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/network_firewall_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ func (c *Compiler) validateHostedWebPolicy(workflowData *WorkflowData) error {
if policy.MaxUses < 0 {
return errors.New("network.hosted-web.max-uses must be a positive integer")
}
if runtimeID == "codex" && policy.MaxUses > 0 && NewCodexEngine().ResolveLLMProvider(workflowData) == LLMProviderOpenAI {
return errors.New("network.hosted-web.max-uses is not supported by Codex standalone web search; remove max-uses to enable hosted web")
}
if err := validateHostedWebDomains("allowed", policy.Allowed); err != nil {
return err
}
Expand Down
15 changes: 14 additions & 1 deletion pkg/workflow/network_firewall_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ func TestValidateHostedWebPolicy(t *testing.T) {
tests := []struct {
name string
engine string
provider LLMProvider
runtime string
inline bool
policy *HostedWebPolicy
Expand All @@ -276,6 +277,18 @@ func TestValidateHostedWebPolicy(t *testing.T) {
engine: "claude",
policy: &HostedWebPolicy{Enabled: true, Allowed: []string{"docs.github.com"}, MaxUses: 1},
},
{
name: "rejects Codex OpenAI standalone max-uses",
engine: "codex",
policy: &HostedWebPolicy{Enabled: true, Allowed: []string{"docs.github.com"}, MaxUses: 1},
wantErr: "network.hosted-web.max-uses is not supported by Codex standalone web search",
},
{
name: "allows Codex GitHub max-uses",
engine: "codex",
provider: LLMProviderGitHub,
policy: &HostedWebPolicy{Enabled: true, Allowed: []string{"docs.github.com"}, MaxUses: 1},
},
{
name: "requires a policy list",
engine: "claude",
Expand Down Expand Up @@ -376,7 +389,7 @@ func TestValidateHostedWebPolicy(t *testing.T) {
compiler.engineCatalog.Register(&EngineDefinition{ID: tt.engine, RuntimeID: tt.runtime})
}
err := compiler.validateHostedWebPolicy(&WorkflowData{
EngineConfig: &EngineConfig{ID: tt.engine, IsInlineDefinition: tt.inline},
EngineConfig: &EngineConfig{ID: tt.engine, IsInlineDefinition: tt.inline, LLMProvider: tt.provider},
NetworkPermissions: network,
})
if tt.wantErr == "" {
Expand Down
Loading