mirror of
https://github.com/openai/codex.git
synced 2026-05-19 12:58:02 +03:00
Compare commits
4 Commits
casey/relo
...
dev/cc/ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bf071758 | ||
|
|
07276e5768 | ||
|
|
4a6f28279e | ||
|
|
fc08419c13 |
1
MODULE.bazel.lock
generated
1
MODULE.bazel.lock
generated
@@ -1141,6 +1141,7 @@
|
||||
"jni_0.21.1": "{\"dependencies\":[{\"name\":\"cesu8\",\"req\":\"^1.1.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"combine\",\"req\":\"^4.1.0\"},{\"name\":\"java-locator\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"jni-sys\",\"req\":\"^0.3.0\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"name\":\"thiserror\",\"req\":\"^1.0.20\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rusty-fork\",\"req\":\"^0.3.0\"},{\"kind\":\"build\",\"name\":\"walkdir\",\"req\":\"^2\"},{\"features\":[\"Win32_Globalization\"],\"name\":\"windows-sys\",\"req\":\"^0.45.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.13.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"invocation\":[\"java-locator\",\"libloading\"]}}",
|
||||
"jobserver_0.1.34": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"getrandom\",\"req\":\"^0.3.2\",\"target\":\"cfg(windows)\"},{\"name\":\"libc\",\"req\":\"^0.2.171\",\"target\":\"cfg(unix)\"},{\"features\":[\"fs\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.28.0\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"}],\"features\":{}}",
|
||||
"js-sys_0.3.85": "{\"dependencies\":[{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"wasm-bindgen/std\"]}}",
|
||||
"jsonptr_0.7.1": "{\"dependencies\":[{\"features\":[\"fancy\"],\"name\":\"miette\",\"optional\":true,\"req\":\"^7.4.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.203\"},{\"features\":[\"alloc\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.119\"},{\"name\":\"syn\",\"optional\":true,\"req\":\"^1.0.109\",\"target\":\"cfg(any())\"},{\"name\":\"toml\",\"optional\":true,\"req\":\"^0.8\"}],\"features\":{\"assign\":[],\"default\":[\"std\",\"serde\",\"json\",\"resolve\",\"assign\",\"delete\"],\"delete\":[\"resolve\"],\"json\":[\"dep:serde_json\",\"serde\"],\"miette\":[\"dep:miette\",\"std\"],\"resolve\":[],\"std\":[\"serde/std\",\"serde_json?/std\"],\"toml\":[\"dep:toml\",\"serde\",\"std\"]}}",
|
||||
"jsonwebtoken_9.3.1": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\")))))\"},{\"name\":\"js-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"pem\",\"optional\":true,\"req\":\"^3\"},{\"features\":[\"std\"],\"name\":\"ring\",\"req\":\"^0.17.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\",\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"simple_asn1\",\"optional\":true,\"req\":\"^0.6\"},{\"features\":[\"wasm-bindgen\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\")))))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.1\"}],\"features\":{\"default\":[\"use_pem\"],\"use_pem\":[\"pem\",\"simple_asn1\"]}}",
|
||||
"keyring_3.6.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"byteorder\",\"optional\":true,\"req\":\"^1.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"derive\",\"wrap_help\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.0-rc.1\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.0-rc.2\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.1\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.5\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2\"},{\"features\":[\"std\"],\"name\":\"linux-keyutils\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"log\",\"req\":\"^0.4.22\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10.66\"},{\"kind\":\"dev\",\"name\":\"rpassword\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"rprompt\",\"req\":\"^2\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"security-framework\",\"optional\":true,\"req\":\"^2\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"name\":\"security-framework\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"whoami\",\"req\":\"^1.5\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Credentials\"],\"name\":\"windows-sys\",\"optional\":true,\"req\":\"^0.60\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"apple-native\":[\"dep:security-framework\"],\"async-io\":[\"zbus?/async-io\"],\"async-secret-service\":[\"dep:secret-service\",\"dep:zbus\"],\"crypto-openssl\":[\"dbus-secret-service?/crypto-openssl\",\"secret-service?/crypto-openssl\"],\"crypto-rust\":[\"dbus-secret-service?/crypto-rust\",\"secret-service?/crypto-rust\"],\"linux-native\":[\"dep:linux-keyutils\"],\"linux-native-async-persistent\":[\"linux-native\",\"async-secret-service\"],\"linux-native-sync-persistent\":[\"linux-native\",\"sync-secret-service\"],\"sync-secret-service\":[\"dep:dbus-secret-service\"],\"tokio\":[\"zbus?/tokio\"],\"vendored\":[\"dbus-secret-service?/vendored\",\"openssl?/vendored\"],\"windows-native\":[\"dep:windows-sys\",\"dep:byteorder\"]}}",
|
||||
"kqueue-sys_1.0.4": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^1.2.1\"},{\"name\":\"libc\",\"req\":\"^0.2.74\"}],\"features\":{}}",
|
||||
|
||||
8
codex-rs/Cargo.lock
generated
8
codex-rs/Cargo.lock
generated
@@ -3757,12 +3757,14 @@ dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-string",
|
||||
"jsonptr",
|
||||
"pretty_assertions",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8088,6 +8090,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonptr"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe"
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.3.1"
|
||||
|
||||
@@ -301,6 +301,7 @@ indexmap = "2.12.0"
|
||||
insta = "1.46.3"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
jsonptr = { version = "0.7.1", default-features = false }
|
||||
jsonwebtoken = "9.3.1"
|
||||
keyring = { version = "3.6", default-features = false }
|
||||
landlock = "0.4.4"
|
||||
|
||||
@@ -16,6 +16,7 @@ codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-utils-string = { workspace = true }
|
||||
jsonptr = { workspace = true }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"base64",
|
||||
"macros",
|
||||
@@ -26,6 +27,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -3,6 +3,7 @@ use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Primitive JSON Schema type names we support in tool definitions.
|
||||
///
|
||||
@@ -33,6 +34,8 @@ pub enum JsonSchemaType {
|
||||
/// Generic JSON-Schema subset needed for our tool definitions.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct JsonSchema {
|
||||
#[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
|
||||
pub schema_ref: Option<String>,
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub schema_type: Option<JsonSchemaType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -52,6 +55,10 @@ pub struct JsonSchema {
|
||||
pub additional_properties: Option<AdditionalProperties>,
|
||||
#[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
|
||||
pub any_of: Option<Vec<JsonSchema>>,
|
||||
#[serde(rename = "$defs", skip_serializing_if = "Option::is_none")]
|
||||
pub defs: Option<BTreeMap<String, JsonSchema>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub definitions: Option<BTreeMap<String, JsonSchema>>,
|
||||
}
|
||||
|
||||
impl JsonSchema {
|
||||
@@ -149,6 +156,7 @@ impl From<JsonSchema> for AdditionalProperties {
|
||||
pub fn parse_tool_input_schema(input_schema: &JsonValue) -> Result<JsonSchema, serde_json::Error> {
|
||||
let mut input_schema = input_schema.clone();
|
||||
sanitize_json_schema(&mut input_schema);
|
||||
prune_unreachable_definitions(&mut input_schema);
|
||||
let schema: JsonSchema = serde_json::from_value(input_schema)?;
|
||||
if matches!(
|
||||
schema.schema_type,
|
||||
@@ -163,6 +171,7 @@ pub fn parse_tool_input_schema(input_schema: &JsonValue) -> Result<JsonSchema, s
|
||||
/// schema representation. This function:
|
||||
/// - Ensures every typed schema object has a `"type"` when required.
|
||||
/// - Preserves explicit `anyOf`.
|
||||
/// - Preserves `$ref` and reachable local `$defs` / `definitions`.
|
||||
/// - Collapses `const` into single-value `enum`.
|
||||
/// - Fills required child fields for object/array schema types, including
|
||||
/// nullable unions, with permissive defaults when absent.
|
||||
@@ -200,6 +209,8 @@ fn sanitize_json_schema(value: &mut JsonValue) {
|
||||
if let Some(value) = map.get_mut("anyOf") {
|
||||
sanitize_json_schema(value);
|
||||
}
|
||||
sanitize_schema_table(map, "$defs");
|
||||
sanitize_schema_table(map, "definitions");
|
||||
|
||||
if let Some(const_value) = map.remove("const") {
|
||||
map.insert("enum".to_string(), JsonValue::Array(vec![const_value]));
|
||||
@@ -207,7 +218,7 @@ fn sanitize_json_schema(value: &mut JsonValue) {
|
||||
|
||||
let mut schema_types = normalized_schema_types(map);
|
||||
|
||||
if schema_types.is_empty() && map.contains_key("anyOf") {
|
||||
if schema_types.is_empty() && (map.contains_key("$ref") || map.contains_key("anyOf")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,6 +252,29 @@ fn sanitize_json_schema(value: &mut JsonValue) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitize a schema definition table before deserializing into `JsonSchema`.
|
||||
///
|
||||
/// Definition tables must be objects. Codex keeps valid definition tables and
|
||||
/// recursively applies the same compatibility lowering used for inline schemas,
|
||||
/// but drops malformed tables so `strict: false` tool registration degrades
|
||||
/// gracefully instead of failing on an unreachable or invalid definition table.
|
||||
fn sanitize_schema_table(map: &mut serde_json::Map<String, JsonValue>, key: &str) {
|
||||
let should_remove = match map.get_mut(key) {
|
||||
Some(JsonValue::Object(definitions)) => {
|
||||
for definition in definitions.values_mut() {
|
||||
sanitize_json_schema(definition);
|
||||
}
|
||||
false
|
||||
}
|
||||
Some(_) => true,
|
||||
None => false,
|
||||
};
|
||||
|
||||
if should_remove {
|
||||
map.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_default_children_for_schema_types(
|
||||
map: &mut serde_json::Map<String, JsonValue>,
|
||||
schema_types: &[JsonSchemaPrimitiveType],
|
||||
@@ -257,6 +291,196 @@ fn ensure_default_children_for_schema_types(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum DefinitionTable {
|
||||
Defs,
|
||||
Definitions,
|
||||
}
|
||||
|
||||
impl DefinitionTable {
|
||||
fn key(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Defs => "$defs",
|
||||
Self::Definitions => "definitions",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct DefinitionPointer {
|
||||
table: DefinitionTable,
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// Prune unused root definition entries to avoid sending tokens for definitions
|
||||
/// the tool schema never references.
|
||||
fn prune_unreachable_definitions(value: &mut JsonValue) {
|
||||
let reachable = collect_reachable_definitions(value);
|
||||
let JsonValue::Object(map) = value else {
|
||||
return;
|
||||
};
|
||||
|
||||
prune_schema_table(map, DefinitionTable::Defs, &reachable);
|
||||
prune_schema_table(map, DefinitionTable::Definitions, &reachable);
|
||||
}
|
||||
|
||||
fn prune_schema_table(
|
||||
map: &mut serde_json::Map<String, JsonValue>,
|
||||
table: DefinitionTable,
|
||||
reachable: &BTreeSet<DefinitionPointer>,
|
||||
) {
|
||||
let Some(JsonValue::Object(definitions)) = map.get_mut(table.key()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
definitions.retain(|name, _| {
|
||||
reachable.contains(&DefinitionPointer {
|
||||
table: table.clone(),
|
||||
name: name.clone(),
|
||||
})
|
||||
});
|
||||
|
||||
if definitions.is_empty() {
|
||||
map.remove(table.key());
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_reachable_definitions(value: &JsonValue) -> BTreeSet<DefinitionPointer> {
|
||||
let mut reachable = BTreeSet::new();
|
||||
let mut pending = Vec::new();
|
||||
|
||||
collect_refs_outside_definitions(value, &mut pending);
|
||||
|
||||
while let Some(pointer) = pending.pop() {
|
||||
if !reachable.insert(pointer.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(definition) = definition_for_pointer(value, &pointer) {
|
||||
collect_refs(definition, &mut pending);
|
||||
}
|
||||
}
|
||||
|
||||
reachable
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum RefCollectionContext {
|
||||
SchemaObject,
|
||||
PropertiesMap,
|
||||
}
|
||||
|
||||
fn collect_refs_outside_definitions(value: &JsonValue, refs: &mut Vec<DefinitionPointer>) {
|
||||
collect_refs_outside_definitions_in_context(value, refs, RefCollectionContext::SchemaObject);
|
||||
}
|
||||
|
||||
fn collect_refs_outside_definitions_in_context(
|
||||
value: &JsonValue,
|
||||
refs: &mut Vec<DefinitionPointer>,
|
||||
context: RefCollectionContext,
|
||||
) {
|
||||
match value {
|
||||
JsonValue::Array(values) => {
|
||||
for value in values {
|
||||
collect_refs_outside_definitions_in_context(
|
||||
value,
|
||||
refs,
|
||||
RefCollectionContext::SchemaObject,
|
||||
);
|
||||
}
|
||||
}
|
||||
JsonValue::Object(map) => match context {
|
||||
RefCollectionContext::SchemaObject => {
|
||||
collect_ref_from_map(map, refs);
|
||||
for (key, value) in map {
|
||||
if key == "$defs" || key == "definitions" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let child_context = if key == "properties" {
|
||||
RefCollectionContext::PropertiesMap
|
||||
} else {
|
||||
RefCollectionContext::SchemaObject
|
||||
};
|
||||
collect_refs_outside_definitions_in_context(value, refs, child_context);
|
||||
}
|
||||
}
|
||||
RefCollectionContext::PropertiesMap => {
|
||||
for value in map.values() {
|
||||
collect_refs_outside_definitions_in_context(
|
||||
value,
|
||||
refs,
|
||||
RefCollectionContext::SchemaObject,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_refs(value: &JsonValue, refs: &mut Vec<DefinitionPointer>) {
|
||||
match value {
|
||||
JsonValue::Array(values) => {
|
||||
for value in values {
|
||||
collect_refs(value, refs);
|
||||
}
|
||||
}
|
||||
JsonValue::Object(map) => {
|
||||
collect_ref_from_map(map, refs);
|
||||
for value in map.values() {
|
||||
collect_refs(value, refs);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_ref_from_map(
|
||||
map: &serde_json::Map<String, JsonValue>,
|
||||
refs: &mut Vec<DefinitionPointer>,
|
||||
) {
|
||||
if let Some(JsonValue::String(schema_ref)) = map.get("$ref")
|
||||
&& let Some(pointer) = parse_local_definition_ref(schema_ref)
|
||||
{
|
||||
refs.push(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
fn definition_for_pointer<'a>(
|
||||
value: &'a JsonValue,
|
||||
pointer: &DefinitionPointer,
|
||||
) -> Option<&'a JsonValue> {
|
||||
let JsonValue::Object(map) = value else {
|
||||
return None;
|
||||
};
|
||||
|
||||
map.get(pointer.table.key())
|
||||
.and_then(JsonValue::as_object)
|
||||
.and_then(|definitions| definitions.get(&pointer.name))
|
||||
}
|
||||
|
||||
fn parse_local_definition_ref(schema_ref: &str) -> Option<DefinitionPointer> {
|
||||
let fragment = schema_ref.strip_prefix('#')?;
|
||||
let pointer = urlencoding::decode(fragment).ok()?;
|
||||
let pointer = jsonptr::Pointer::parse(pointer.as_ref()).ok()?;
|
||||
|
||||
let (table_token, pointer) = pointer.split_front()?;
|
||||
let table = match table_token.decoded().as_ref() {
|
||||
"$defs" => DefinitionTable::Defs,
|
||||
"definitions" => DefinitionTable::Definitions,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Responses API non-strict mode accepts nested local refs such as
|
||||
// `#/$defs/User/properties/name`, so keep the parent definition reachable.
|
||||
let (name, _) = pointer.split_front()?;
|
||||
Some(DefinitionPointer {
|
||||
table,
|
||||
name: name.decoded().into_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
fn normalized_schema_types(
|
||||
map: &serde_json::Map<String, JsonValue>,
|
||||
) -> Vec<JsonSchemaPrimitiveType> {
|
||||
|
||||
@@ -848,3 +848,479 @@ fn parse_tool_input_schema_preserves_string_enum_constraints() {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_input_schema_preserves_refs_and_prunes_unreachable_defs() {
|
||||
// Example schema shape:
|
||||
// {
|
||||
// "type": "object",
|
||||
// "properties": { "user": { "$ref": "#/$defs/User" } },
|
||||
// "$defs": {
|
||||
// "User": { "type": "object", "properties": { "name": { "type": "string" } } },
|
||||
// "Unused": { "type": "string" }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Expected normalization behavior:
|
||||
// - Local `$ref` is preserved as a schema hint.
|
||||
// - Reachable `$defs` entries stay attached to the root schema.
|
||||
// - Unreachable `$defs` entries are pruned.
|
||||
let schema = parse_tool_input_schema(&serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {"$ref": "#/$defs/User"}
|
||||
},
|
||||
"$defs": {
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"Unused": {"type": "string"}
|
||||
}
|
||||
}))
|
||||
.expect("parse schema");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
JsonSchema {
|
||||
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
|
||||
properties: Some(BTreeMap::from([(
|
||||
"user".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/$defs/User".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
defs: Some(BTreeMap::from([(
|
||||
"User".to_string(),
|
||||
JsonSchema::object(
|
||||
BTreeMap::from([(
|
||||
"name".to_string(),
|
||||
JsonSchema::string(/*description*/ None),
|
||||
)]),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None,
|
||||
),
|
||||
)])),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_input_schema_preserves_refs_from_properties_named_def_tables() {
|
||||
// Example schema shape:
|
||||
// {
|
||||
// "type": "object",
|
||||
// "properties": {
|
||||
// "$defs": { "$ref": "#/$defs/User" }
|
||||
// },
|
||||
// "$defs": { "User": { "type": "string" }, "Unused": { "type": "boolean" } }
|
||||
// }
|
||||
//
|
||||
// Expected normalization behavior:
|
||||
// - A property named like the `$defs` keyword is treated as a user field
|
||||
// while traversing `properties`.
|
||||
// - Refs from that property schema still mark root definitions reachable.
|
||||
// - Unreferenced root definitions are still pruned.
|
||||
let schema = parse_tool_input_schema(&serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$defs": {"$ref": "#/$defs/User"}
|
||||
},
|
||||
"$defs": {
|
||||
"User": {"type": "string"},
|
||||
"Unused": {"type": "boolean"}
|
||||
}
|
||||
}))
|
||||
.expect("parse schema");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
JsonSchema {
|
||||
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
|
||||
properties: Some(BTreeMap::from([(
|
||||
"$defs".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/$defs/User".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
defs: Some(BTreeMap::from([(
|
||||
"User".to_string(),
|
||||
JsonSchema::string(/*description*/ None),
|
||||
)])),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_input_schema_handles_cyclic_local_refs() {
|
||||
// Example schema shape:
|
||||
// {
|
||||
// "type": "object",
|
||||
// "properties": { "node": { "$ref": "#/$defs/Node" } },
|
||||
// "$defs": {
|
||||
// "Node": {
|
||||
// "type": "object",
|
||||
// "properties": { "next": { "$ref": "#/$defs/Node" } }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Expected normalization behavior:
|
||||
// - Recursive refs are preserved.
|
||||
// - Pruning traversal terminates after visiting each local target once.
|
||||
// - Responses API handles this recursive local-ref shape correctly.
|
||||
let schema = parse_tool_input_schema(&serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node": {"$ref": "#/$defs/Node"}
|
||||
},
|
||||
"$defs": {
|
||||
"Node": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"next": {"$ref": "#/$defs/Node"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.expect("parse schema");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
JsonSchema {
|
||||
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
|
||||
properties: Some(BTreeMap::from([(
|
||||
"node".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/$defs/Node".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
defs: Some(BTreeMap::from([(
|
||||
"Node".to_string(),
|
||||
JsonSchema::object(
|
||||
BTreeMap::from([(
|
||||
"next".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/$defs/Node".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None,
|
||||
),
|
||||
)])),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_input_schema_preserves_legacy_definitions() {
|
||||
// Example schema shape:
|
||||
// {
|
||||
// "type": "object",
|
||||
// "properties": { "user": { "$ref": "#/definitions/User" } },
|
||||
// "definitions": {
|
||||
// "User": { "type": "object", "properties": { "profile": { "$ref": "#/definitions/Profile" } } },
|
||||
// "Profile": { "type": "object", "properties": { "name": { "type": "string" } } }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Expected normalization behavior:
|
||||
// - Codex preserves legacy `definitions`.
|
||||
// - Reachability follows refs through the legacy definition table.
|
||||
// - Unreachable legacy definition entries are pruned.
|
||||
let schema = parse_tool_input_schema(&serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {"$ref": "#/definitions/User"}
|
||||
},
|
||||
"definitions": {
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profile": {"$ref": "#/definitions/Profile"}
|
||||
}
|
||||
},
|
||||
"Profile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"Unused": {"type": "string"}
|
||||
}
|
||||
}))
|
||||
.expect("parse schema");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
JsonSchema {
|
||||
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
|
||||
properties: Some(BTreeMap::from([(
|
||||
"user".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/definitions/User".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
definitions: Some(BTreeMap::from([
|
||||
(
|
||||
"Profile".to_string(),
|
||||
JsonSchema::object(
|
||||
BTreeMap::from([(
|
||||
"name".to_string(),
|
||||
JsonSchema::string(/*description*/ None),
|
||||
)]),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None,
|
||||
),
|
||||
),
|
||||
(
|
||||
"User".to_string(),
|
||||
JsonSchema::object(
|
||||
BTreeMap::from([(
|
||||
"profile".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/definitions/Profile".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None,
|
||||
),
|
||||
),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_input_schema_preserves_unresolved_and_external_refs() {
|
||||
// Example schema shape:
|
||||
// {
|
||||
// "type": "object",
|
||||
// "properties": {
|
||||
// "missing": { "$ref": "#/$defs/Missing" },
|
||||
// "remote": { "$ref": "https://example.com/schema.json" }
|
||||
// },
|
||||
// "$defs": { "Unused": { "type": "string" } }
|
||||
// }
|
||||
//
|
||||
// Expected normalization behavior:
|
||||
// - Unresolved local refs and external refs are preserved.
|
||||
// - Unreachable local definitions are still pruned.
|
||||
// - Responses API handles these refs correctly during downstream validation.
|
||||
let schema = parse_tool_input_schema(&serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"missing": {"$ref": "#/$defs/Missing"},
|
||||
"remote": {"$ref": "https://example.com/schema.json"}
|
||||
},
|
||||
"$defs": {
|
||||
"Unused": {"type": "string"}
|
||||
}
|
||||
}))
|
||||
.expect("parse schema");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
JsonSchema {
|
||||
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
|
||||
properties: Some(BTreeMap::from([
|
||||
(
|
||||
"missing".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/$defs/Missing".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"remote".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("https://example.com/schema.json".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_input_schema_preserves_nested_defs_ref_parent() {
|
||||
// Example schema shape:
|
||||
// {
|
||||
// "type": "object",
|
||||
// "properties": { "name": { "$ref": "#/$defs/User/properties/name" } },
|
||||
// "$defs": {
|
||||
// "User": { "type": "object", "properties": { "name": { "type": "string" } } },
|
||||
// "name": { "type": "string" },
|
||||
// "Unused": { "type": "boolean" }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Expected normalization behavior:
|
||||
// - The nested JSON Pointer ref remains unchanged.
|
||||
// - The parent root definition is retained so the local ref does not dangle.
|
||||
// - Unreferenced root definitions are still pruned.
|
||||
let schema = parse_tool_input_schema(&serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"$ref": "#/$defs/User/properties/name"}
|
||||
},
|
||||
"$defs": {
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"name": {"type": "string"},
|
||||
"Unused": {"type": "boolean"}
|
||||
}
|
||||
}))
|
||||
.expect("parse schema");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
JsonSchema {
|
||||
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
|
||||
properties: Some(BTreeMap::from([(
|
||||
"name".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/$defs/User/properties/name".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
defs: Some(BTreeMap::from([(
|
||||
"User".to_string(),
|
||||
JsonSchema::object(
|
||||
BTreeMap::from([(
|
||||
"name".to_string(),
|
||||
JsonSchema::string(/*description*/ None),
|
||||
)]),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None,
|
||||
),
|
||||
)])),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_input_schema_preserves_percent_encoded_definition_refs() {
|
||||
// Example schema shape:
|
||||
// {
|
||||
// "type": "object",
|
||||
// "properties": {
|
||||
// "user": { "$ref": "#/$defs/User%20Name" },
|
||||
// "profile": { "$ref": "#/%24defs/Profile%7E0Name" }
|
||||
// },
|
||||
// "$defs": {
|
||||
// "User Name": { "type": "string" },
|
||||
// "Profile~Name": { "type": "string" },
|
||||
// "Unused": { "type": "boolean" }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Expected normalization behavior:
|
||||
// - URI fragment percent encoding is decoded before JSON Pointer `~`
|
||||
// escaping, per RFC 6901 section 6.
|
||||
// - The original `$ref` strings are preserved, but their definition
|
||||
// targets are recognized as reachable and retained.
|
||||
let schema = parse_tool_input_schema(&serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {"$ref": "#/$defs/User%20Name"},
|
||||
"profile": {"$ref": "#/%24defs/Profile%7E0Name"}
|
||||
},
|
||||
"$defs": {
|
||||
"User Name": {"type": "string"},
|
||||
"Profile~Name": {"type": "string"},
|
||||
"Unused": {"type": "boolean"}
|
||||
}
|
||||
}))
|
||||
.expect("parse schema");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
JsonSchema {
|
||||
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
|
||||
properties: Some(BTreeMap::from([
|
||||
(
|
||||
"profile".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/%24defs/Profile%7E0Name".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"user".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/$defs/User%20Name".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
])),
|
||||
defs: Some(BTreeMap::from([
|
||||
(
|
||||
"Profile~Name".to_string(),
|
||||
JsonSchema::string(/*description*/ None),
|
||||
),
|
||||
(
|
||||
"User Name".to_string(),
|
||||
JsonSchema::string(/*description*/ None),
|
||||
),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tool_input_schema_drops_malformed_definition_tables() {
|
||||
// Example schema shape:
|
||||
// {
|
||||
// "type": "object",
|
||||
// "properties": { "user": { "$ref": "#/$defs/User" } },
|
||||
// "$defs": ["not", "an", "object"]
|
||||
// }
|
||||
//
|
||||
// Expected normalization behavior:
|
||||
// - Malformed `$defs` tables are dropped instead of rejecting the schema.
|
||||
// - The unresolved local ref remains visible to the model.
|
||||
let schema = parse_tool_input_schema(&serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {"$ref": "#/$defs/User"}
|
||||
},
|
||||
"$defs": ["not", "an", "object"]
|
||||
}))
|
||||
.expect("parse schema");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
JsonSchema {
|
||||
schema_type: Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object)),
|
||||
properties: Some(BTreeMap::from([(
|
||||
"user".to_string(),
|
||||
JsonSchema {
|
||||
schema_ref: Some("#/$defs/User".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)])),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user