Skip to content

Commit bb25a5b

Browse files
ericjutacodex
andcommitted
chore: sync upstream config js repl and model metadata
Cherry-picks and adapts upstream d87d918, ac8c9fc, e083b6c, 6f87eb0, and c10f95d for this branch. Co-authored-by: Codex <noreply@openai.com>
1 parent a62e06e commit bb25a5b

38 files changed

Lines changed: 572 additions & 57 deletions

File tree

codex-rs/app-server/tests/common/models_cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
3636
default_reasoning_summary: ReasoningSummary::Auto,
3737
support_verbosity: false,
3838
default_verbosity: None,
39-
availability_nux: None,
39+
availability_nux: preset.availability_nux.clone(),
4040
apply_patch_tool_type: None,
4141
web_search_tool_type: Default::default(),
4242
truncation_policy: TruncationPolicyConfig::bytes(/*limit*/ 10_000),

codex-rs/config/src/mcp_types.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,11 @@ pub struct McpServerConfig {
176176
pub tools: HashMap<String, McpServerToolConfig>,
177177
}
178178

179-
/// Raw MCP config shape used for deserialization and JSON Schema generation.
179+
/// Raw MCP config shape used for deserialization and supported-field JSON
180+
/// Schema generation.
181+
///
182+
/// Fields that are accepted only to produce targeted validation errors should
183+
/// be skipped in the generated schema.
180184
///
181185
/// Keep `TryFrom<RawMcpServerConfig> for McpServerConfig` exhaustively
182186
/// destructuring this struct so new TOML fields cannot be added here without
@@ -200,6 +204,7 @@ pub struct RawMcpServerConfig {
200204

201205
// streamable_http
202206
pub url: Option<String>,
207+
#[schemars(skip)]
203208
pub bearer_token: Option<String>,
204209
pub bearer_token_env_var: Option<String>,
205210

codex-rs/core/config.schema.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,7 +1575,7 @@
15751575
},
15761576
"RawMcpServerConfig": {
15771577
"additionalProperties": false,
1578-
"description": "Raw MCP config shape used for deserialization and JSON Schema generation.\n\nKeep `TryFrom<RawMcpServerConfig> for McpServerConfig` exhaustively destructuring this struct so new TOML fields cannot be added here without updating the validation/mapping logic that produces [`McpServerConfig`].",
1578+
"description": "Raw MCP config shape used for deserialization and supported-field JSON Schema generation.\n\nFields that are accepted only to produce targeted validation errors should be skipped in the generated schema.\n\nKeep `TryFrom<RawMcpServerConfig> for McpServerConfig` exhaustively destructuring this struct so new TOML fields cannot be added here without updating the validation/mapping logic that produces [`McpServerConfig`].",
15791579
"properties": {
15801580
"args": {
15811581
"default": null,
@@ -1584,9 +1584,6 @@
15841584
},
15851585
"type": "array"
15861586
},
1587-
"bearer_token": {
1588-
"type": "string"
1589-
},
15901587
"bearer_token_env_var": {
15911588
"type": "string"
15921589
},
@@ -3003,4 +3000,4 @@
30033000
},
30043001
"title": "ConfigToml",
30053002
"type": "object"
3006-
}
3003+
}

codex-rs/core/src/agents_md.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ fn render_js_repl_instructions(config: &Config) -> Option<String> {
5656
"- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n",
5757
);
5858
section.push_str("- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n");
59-
section.push_str("- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n");
59+
section.push_str("- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }` containing encoded PNG/JPEG/WebP/GIF bytes, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n");
6060
section.push_str("- `codex.tool(...)` and `codex.emitImage(...)` keep stable helper identities across cells. Saved references and persisted objects can reuse them in later cells, but async callbacks that fire after a cell finishes still fail because no exec is active.\n");
6161
section.push_str("- Request full-resolution image processing with `detail: \"original\"` only when the `view_image` tool schema includes a `detail` argument. The same availability applies to `codex.emitImage(...)`: if `view_image.detail` is present, you may also pass `detail: \"original\"` there. Use this when high-fidelity image perception or precise localization is needed, especially for CUA agents.\n");
6262
section.push_str("- Raw MCP image blocks can request the same behavior by returning `_meta: { \"codex/imageDetail\": \"original\" }` on the image content item.\n");

codex-rs/core/src/agents_md_tests.rs

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

codex-rs/core/src/config/agent_roles.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ pub(crate) async fn load_agent_roles(
3333
for layer in layers {
3434
let mut layer_roles: BTreeMap<String, AgentRoleConfig> = BTreeMap::new();
3535
let mut declared_role_files = BTreeSet::new();
36-
let agents_toml = match agents_toml_from_layer(&layer.config) {
36+
let config_folder = layer.config_folder();
37+
let agents_toml = match agents_toml_from_layer(&layer.config, config_folder.as_deref()) {
3738
Ok(agents_toml) => agents_toml,
3839
Err(err) => {
3940
push_agent_role_warning(startup_warnings, err);
@@ -169,11 +170,16 @@ fn merge_missing_role_fields(role: &mut AgentRoleConfig, fallback: &AgentRoleCon
169170
.or(fallback.nickname_candidates.clone());
170171
}
171172

172-
fn agents_toml_from_layer(layer_toml: &TomlValue) -> std::io::Result<Option<AgentsToml>> {
173+
fn agents_toml_from_layer(
174+
layer_toml: &TomlValue,
175+
config_base_dir: Option<&Path>,
176+
) -> std::io::Result<Option<AgentsToml>> {
173177
let Some(agents_toml) = layer_toml.get("agents") else {
174178
return Ok(None);
175179
};
176180

181+
// AbsolutePathBufGuard resolves relative paths while it remains in scope.
182+
let _guard = config_base_dir.map(AbsolutePathBufGuard::new);
177183
agents_toml
178184
.clone()
179185
.try_into()

codex-rs/core/src/config/config_tests.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3718,6 +3718,63 @@ nickname_candidates = ["Hypatia", "Noether"]
37183718
Ok(())
37193719
}
37203720

3721+
#[tokio::test]
3722+
async fn agent_role_relative_config_file_resolves_from_config_layer() -> std::io::Result<()> {
3723+
let codex_home = TempDir::new()?;
3724+
let role_config_path = codex_home.path().join("agents").join("researcher.toml");
3725+
tokio::fs::create_dir_all(
3726+
role_config_path
3727+
.parent()
3728+
.expect("role config should have a parent directory"),
3729+
)
3730+
.await?;
3731+
tokio::fs::write(
3732+
&role_config_path,
3733+
"developer_instructions = \"Research carefully\"\nmodel = \"gpt-5\"",
3734+
)
3735+
.await?;
3736+
let layer_config = toml::from_str(
3737+
r#"[agents.researcher]
3738+
description = "Research role"
3739+
config_file = "./agents/researcher.toml"
3740+
"#,
3741+
)
3742+
.expect("agent role layer config should parse");
3743+
let config_layer_stack = crate::config_loader::ConfigLayerStack::new(
3744+
vec![crate::config_loader::ConfigLayerEntry::new(
3745+
codex_app_server_protocol::ConfigLayerSource::User {
3746+
file: codex_home.path().join(CONFIG_TOML_FILE).abs(),
3747+
},
3748+
layer_config,
3749+
)],
3750+
Default::default(),
3751+
crate::config_loader::ConfigRequirementsToml::default(),
3752+
)
3753+
.map_err(std::io::Error::other)?;
3754+
3755+
let config = Config::load_config_with_layer_stack(
3756+
LOCAL_FS.as_ref(),
3757+
ConfigToml::default(),
3758+
ConfigOverrides {
3759+
cwd: Some(codex_home.path().to_path_buf()),
3760+
..Default::default()
3761+
},
3762+
codex_home.abs(),
3763+
config_layer_stack,
3764+
)
3765+
.await?;
3766+
3767+
assert_eq!(
3768+
config
3769+
.agent_roles
3770+
.get("researcher")
3771+
.and_then(|role| role.config_file.as_ref()),
3772+
Some(&role_config_path)
3773+
);
3774+
3775+
Ok(())
3776+
}
3777+
37213778
#[tokio::test]
37223779
async fn agent_role_file_metadata_overrides_config_toml_metadata() -> std::io::Result<()> {
37233780
let codex_home = TempDir::new()?;

codex-rs/core/src/config/schema_tests.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,23 @@ Run `just write-config-schema` to overwrite with your changes.\n\n{diff}"
5353
"fixture should match exactly with generated schema"
5454
);
5555
}
56+
57+
#[test]
58+
fn config_schema_hides_unsupported_inline_mcp_bearer_token() {
59+
let schema_json = config_schema_json().expect("serialize config schema");
60+
let schema_value: serde_json::Value =
61+
serde_json::from_slice(&schema_json).expect("decode schema json");
62+
let properties = schema_value
63+
.pointer("/definitions/RawMcpServerConfig/properties")
64+
.expect("RawMcpServerConfig properties should exist")
65+
.as_object()
66+
.expect("RawMcpServerConfig properties should be an object");
67+
68+
assert_eq!(
69+
(
70+
properties.contains_key("bearer_token"),
71+
properties.contains_key("bearer_token_env_var"),
72+
),
73+
(false, true),
74+
);
75+
}

codex-rs/core/src/tools/handlers/unified_exec.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use crate::unified_exec::UnifiedExecError;
2424
use crate::unified_exec::UnifiedExecProcessManager;
2525
use crate::unified_exec::WriteStdinRequest;
2626
use crate::unified_exec::generate_chunk_id;
27+
use crate::unified_exec::resolve_max_tokens;
2728
use codex_features::Feature;
2829
use codex_otel::SessionTelemetry;
2930
use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;
@@ -32,6 +33,7 @@ use codex_protocol::protocol::EventMsg;
3233
use codex_protocol::protocol::TerminalInteractionEvent;
3334
use codex_shell_command::is_safe_command::is_known_safe_command;
3435
use codex_tools::UnifiedExecShellMode;
36+
use codex_utils_output_truncation::TruncationPolicy;
3537
use codex_utils_output_truncation::approx_token_count;
3638
use serde::Deserialize;
3739
use std::path::PathBuf;
@@ -88,6 +90,13 @@ fn default_tty() -> bool {
8890
false
8991
}
9092

93+
fn effective_max_output_tokens(
94+
max_output_tokens: Option<usize>,
95+
truncation_policy: TruncationPolicy,
96+
) -> usize {
97+
resolve_max_tokens(max_output_tokens).min(truncation_policy.token_budget())
98+
}
99+
91100
impl ToolHandler for UnifiedExecHandler {
92101
type Output = ExecCommandToolOutput;
93102

@@ -233,6 +242,8 @@ impl ToolHandler for UnifiedExecHandler {
233242
prefix_rule,
234243
..
235244
} = args;
245+
let max_output_tokens =
246+
effective_max_output_tokens(max_output_tokens, turn.truncation_policy);
236247

237248
let exec_permission_approvals_enabled =
238249
session.features().enabled(Feature::ExecPermissionApprovals);
@@ -312,7 +323,7 @@ impl ToolHandler for UnifiedExecHandler {
312323
chunk_id: String::new(),
313324
wall_time: std::time::Duration::ZERO,
314325
raw_output: output.into_text().into_bytes(),
315-
max_output_tokens: None,
326+
max_output_tokens: Some(max_output_tokens),
316327
process_id: None,
317328
exit_code: None,
318329
original_token_count: None,
@@ -328,7 +339,7 @@ impl ToolHandler for UnifiedExecHandler {
328339
hook_command: hook_command.clone(),
329340
process_id,
330341
yield_time_ms,
331-
max_output_tokens,
342+
max_output_tokens: Some(max_output_tokens),
332343
workdir,
333344
network: context.turn.network.clone(),
334345
tty,
@@ -353,7 +364,7 @@ impl ToolHandler for UnifiedExecHandler {
353364
chunk_id: generate_chunk_id(),
354365
wall_time: output.duration,
355366
raw_output: output_text.into_bytes(),
356-
max_output_tokens,
367+
max_output_tokens: Some(max_output_tokens),
357368
// Sandbox denial is terminal, so there is no live
358369
// process for write_stdin to resume.
359370
process_id: None,
@@ -371,12 +382,14 @@ impl ToolHandler for UnifiedExecHandler {
371382
}
372383
"write_stdin" => {
373384
let args: WriteStdinArgs = parse_arguments(&arguments)?;
385+
let max_output_tokens =
386+
effective_max_output_tokens(args.max_output_tokens, turn.truncation_policy);
374387
let response = manager
375388
.write_stdin(WriteStdinRequest {
376389
process_id: args.session_id,
377390
input: &args.chars,
378391
yield_time_ms: args.yield_time_ms,
379-
max_output_tokens: args.max_output_tokens,
392+
max_output_tokens: Some(max_output_tokens),
380393
})
381394
.await
382395
.map_err(|err| {

codex-rs/core/src/tools/js_repl/kernel.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,7 @@ function encodeByteImage(bytes, mimeType, detail) {
12141214
if (typeof mimeType !== "string" || !mimeType) {
12151215
throw new Error("codex.emitImage expected a non-empty mimeType");
12161216
}
1217+
assertEmitImageMimeType(mimeType);
12171218
const image_url = `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`;
12181219
return { image_url, detail };
12191220
}
@@ -1240,9 +1241,42 @@ function normalizeEmitImageUrl(value) {
12401241
if (!/^data:/i.test(value)) {
12411242
throw new Error("codex.emitImage only accepts data URLs");
12421243
}
1244+
const mimeType = parseDataUrlMimeType(value);
1245+
assertEmitImageMimeType(mimeType);
12431246
return value;
12441247
}
12451248

1249+
const SUPPORTED_EMIT_IMAGE_MIME_TYPES = [
1250+
"image/png",
1251+
"image/jpeg",
1252+
"image/webp",
1253+
"image/gif",
1254+
];
1255+
1256+
function parseDataUrlMimeType(dataUrl) {
1257+
const commaIndex = dataUrl.indexOf(",");
1258+
if (commaIndex < 0) {
1259+
throw new Error("codex.emitImage expected a valid image data URL");
1260+
}
1261+
const mediaType = dataUrl.slice("data:".length, commaIndex).split(";")[0];
1262+
if (!mediaType) {
1263+
throw new Error("codex.emitImage expected image data URL to include a MIME type");
1264+
}
1265+
return mediaType;
1266+
}
1267+
1268+
function assertEmitImageMimeType(mimeType) {
1269+
const normalized = typeof mimeType === "string" ? mimeType.toLowerCase() : "";
1270+
if (!SUPPORTED_EMIT_IMAGE_MIME_TYPES.includes(normalized)) {
1271+
const supportedTypes = `${SUPPORTED_EMIT_IMAGE_MIME_TYPES.slice(0, -1).join(", ")}, or ${
1272+
SUPPORTED_EMIT_IMAGE_MIME_TYPES[SUPPORTED_EMIT_IMAGE_MIME_TYPES.length - 1]
1273+
}`;
1274+
throw new Error(
1275+
`codex.emitImage only supports ${supportedTypes}`,
1276+
);
1277+
}
1278+
}
1279+
12461280
function parseInputImageItem(value) {
12471281
if (!isPlainObject(value) || value.type !== "input_image") {
12481282
return null;

0 commit comments

Comments
 (0)