codex-tools: extract shared tool schema parsing (#15923)

## Why

`parse_tool_input_schema` and the supporting `JsonSchema` model were
living in `core/src/tools/spec.rs`, but they already serve callers
outside `codex-core`.

Keeping that shared schema parsing logic inside `codex-core` makes the
crate boundary harder to reason about and works against the guidance in
`AGENTS.md` to avoid growing `codex-core` when reusable code can live
elsewhere.

This change takes the first extraction step by moving the schema parsing
primitive into its own crate while keeping the rest of the tool-spec
assembly in `codex-core`.

## What changed

- added a new `codex-tools` crate under `codex-rs/tools`
- moved the shared tool input schema model and sanitizer/parser into
`tools/src/json_schema.rs`
- kept `tools/src/lib.rs` exports-only, with the module-level unit tests
split into `json_schema_tests.rs`
- updated `codex-core` to use `codex-tools::JsonSchema` and re-export
`parse_tool_input_schema`
- updated `codex-app-server` dynamic tool validation to depend on
`codex-tools` directly instead of reaching through `codex-core`
- wired the new crate into the Cargo workspace and Bazel build graph
This commit is contained in:
Michael Bolin
2026-03-26 17:03:35 -07:00
committed by GitHub
parent a27cd2d281
commit 44d28f500f
14 changed files with 394 additions and 183 deletions

View File

@@ -0,0 +1,98 @@
use super::AdditionalProperties;
use super::JsonSchema;
use super::parse_tool_input_schema;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
#[test]
fn parse_tool_input_schema_coerces_boolean_schemas() {
let schema = parse_tool_input_schema(&serde_json::json!(true)).expect("parse schema");
assert_eq!(schema, JsonSchema::String { description: None });
}
#[test]
fn parse_tool_input_schema_infers_object_shape_and_defaults_properties() {
let schema = parse_tool_input_schema(&serde_json::json!({
"properties": {
"query": {"description": "search query"}
}
}))
.expect("parse schema");
assert_eq!(
schema,
JsonSchema::Object {
properties: BTreeMap::from([(
"query".to_string(),
JsonSchema::String {
description: Some("search query".to_string()),
},
)]),
required: None,
additional_properties: None,
}
);
}
#[test]
fn parse_tool_input_schema_normalizes_integer_and_missing_array_items() {
let schema = parse_tool_input_schema(&serde_json::json!({
"type": "object",
"properties": {
"page": {"type": "integer"},
"tags": {"type": "array"}
}
}))
.expect("parse schema");
assert_eq!(
schema,
JsonSchema::Object {
properties: BTreeMap::from([
("page".to_string(), JsonSchema::Number { description: None },),
(
"tags".to_string(),
JsonSchema::Array {
items: Box::new(JsonSchema::String { description: None }),
description: None,
},
),
]),
required: None,
additional_properties: None,
}
);
}
#[test]
fn parse_tool_input_schema_sanitizes_additional_properties_schema() {
let schema = parse_tool_input_schema(&serde_json::json!({
"type": "object",
"additionalProperties": {
"required": ["value"],
"properties": {
"value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
}
}
}))
.expect("parse schema");
assert_eq!(
schema,
JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: Some(AdditionalProperties::Schema(Box::new(
JsonSchema::Object {
properties: BTreeMap::from([(
"value".to_string(),
JsonSchema::String { description: None },
)]),
required: Some(vec!["value".to_string()]),
additional_properties: None,
},
))),
}
);
}