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
2 changes: 1 addition & 1 deletion Cargo.lock

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

21 changes: 21 additions & 0 deletions crates/graphql-orm-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ checkpoint facts. For the current workspace baseline and active gates, use the
[implementation status](docs/implementation-status.md) and the central
[AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md).

## [0.88.1] - 2026-08-22

Persistent schema module: **0.63.0** (unchanged from 0.88.0).

### Fixed

- The Codex app-server schema projector now preserves bounded nullable scalar
`type` arrays used by the crate-authored FixedBroker definitions. Codex
0.148.0 accepts these schemas directly; previously the adapter rejected the
discovery and execute definitions before starting a readiness turn.

### Security

- Type-array projection is limited to unique combinations of `string`,
`integer`, `number`, `boolean`, and `null`, requires `null` plus at least one
non-null scalar, validates compatible enums and constraints, and still
rejects object, array, unbounded, malformed, or unknown union shapes.

There is no schema, data, protected-payload, GraphQL SDL, backup or restore
migration in this release.

## [0.88.0] - 2026-08-22

Persistent schema module: **0.63.0** (unchanged from 0.87.0).
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm-ai"
version = "0.88.0"
version = "0.88.1"
edition = "2024"
authors = ["Toby Martin <toby@dastari.net>"]
description = "Project-agnostic AI agent runtime for graphql-orm applications"
Expand Down
18 changes: 18 additions & 0 deletions crates/graphql-orm-ai/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ they describe. For the current workspace baseline and active delivery gates,
use [implementation status](docs/implementation-status.md) and the central
[AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md).

## 0.88.0 to 0.88.1: Codex FixedBroker schema projection

Adopt `graphql-orm-ai` 0.88.1 from one reviewed full monorepo revision. The AI
schema module remains **0.63.0**. There is no database, data, table, column,
index, constraint, backfill, GraphQL SDL, protected-payload, backup or restore
migration.

Hosts using the Codex app-server adapter should rerun readiness against their
complete capability surface. The adapter now projects the crate-authored
FixedBroker discovery, describe, and execute definitions without rewriting
their bounded nullable scalar `type` arrays. Codex 0.148.0 was measured to
accept the preserved form and deliver the offered dynamic tool directly.

No host API changes are required. Unknown, duplicate, non-nullable, structural,
or otherwise malformed type unions continue to fail closed before a provider
turn. Keep the 0.88.0 closed launch profile, full-surface readiness guard, and
negative native-item checks unchanged.

## 0.87.0 to 0.88.0: direct GPT-5.6 dynamic tools on Codex 0.148.0

Adopt `graphql-orm-ai` 0.88.0 from one reviewed full monorepo revision. The AI
Expand Down
9 changes: 8 additions & 1 deletion crates/graphql-orm-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ for AI, ORM, storage, backup, and tool-profile packages:

```toml
[dependencies]
graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.87.0", default-features = false, features = ["sqlite"] }
graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.88.1", default-features = false, features = ["sqlite"] }
```

Exactly one persistence backend is required: `sqlite` (default), `postgres`,
Expand Down Expand Up @@ -159,6 +159,13 @@ false. The protocol actor rejects any native item that is nevertheless
emitted. Reverify both direct delivery and the negative native-item matrix
before upgrading Codex.

The Codex schema projector preserves bounded nullable scalar `type` arrays in
the crate-authored FixedBroker definitions. It does not pass through arbitrary
JSON Schema unions: only unique combinations of supported scalar types plus
`null`, with compatible enums and constraints, are admitted. Full-surface
readiness must project all three FixedBroker definitions before the host is
ready.

See the [session reliability adoption contract](docs/session-reliability-adoption.md).

## Features and capability boundary
Expand Down
201 changes: 200 additions & 1 deletion crates/graphql-orm-ai/src/providers/codex_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4795,6 +4795,9 @@ fn project_codex_schema_node(schema: &Value, depth: usize) -> Result<Value, Prov
}) {
return Err(ProviderError::Rejected);
}
if let Some(schema_types) = object.get("type").and_then(Value::as_array) {
return project_codex_nullable_scalar_union(object, schema_types);
}
let schema_type = object
.get("type")
.and_then(Value::as_str)
Expand Down Expand Up @@ -4982,6 +4985,129 @@ fn project_codex_schema_node(schema: &Value, depth: usize) -> Result<Value, Prov
Ok(Value::Object(projected))
}

fn project_codex_nullable_scalar_union(
object: &serde_json::Map<String, Value>,
schema_types: &[Value],
) -> Result<Value, ProviderError> {
if !(2..=5).contains(&schema_types.len())
|| object.keys().any(|key| {
matches!(
key.as_str(),
"properties"
| "required"
| "additionalProperties"
| "items"
| "minItems"
| "maxItems"
| "uniqueItems"
)
})
{
return Err(ProviderError::Rejected);
}
let mut unique_types = BTreeSet::new();
for schema_type in schema_types {
let schema_type = schema_type.as_str().ok_or(ProviderError::Rejected)?;
if !matches!(
schema_type,
"string" | "integer" | "number" | "boolean" | "null"
) || !unique_types.insert(schema_type)
{
return Err(ProviderError::Rejected);
}
}
if !unique_types.contains("null") || unique_types.len() < 2 {
return Err(ProviderError::Rejected);
}

let description = object
.get("description")
.map(|value| value.as_str().ok_or(ProviderError::Rejected))
.transpose()?
.unwrap_or_default();
validate_codex_schema_description(description)?;

let mut constraint_notes = Vec::new();
let minimum_length = optional_u64(object, "minLength")?;
let maximum_length = optional_u64(object, "maxLength")?;
if (minimum_length.is_some() || maximum_length.is_some()) && !unique_types.contains("string") {
return Err(ProviderError::Rejected);
}
if minimum_length
.zip(maximum_length)
.is_some_and(|(minimum, maximum)| minimum > maximum)
{
return Err(ProviderError::Rejected);
}
if let Some(minimum) = minimum_length {
constraint_notes.push(format!("minimum length {minimum}"));
}
if let Some(maximum) = maximum_length {
constraint_notes.push(format!("maximum length {maximum}"));
}

let minimum = optional_number(object, "minimum")?;
let maximum = optional_number(object, "maximum")?;
if (minimum.is_some() || maximum.is_some())
&& !unique_types.contains("integer")
&& !unique_types.contains("number")
{
return Err(ProviderError::Rejected);
}
if minimum
.zip(maximum)
.is_some_and(|(minimum, maximum)| minimum > maximum)
{
return Err(ProviderError::Rejected);
}
if let Some(minimum) = minimum {
constraint_notes.push(format!("minimum {minimum}"));
}
if let Some(maximum) = maximum {
constraint_notes.push(format!("maximum {maximum}"));
}

let mut projected =
serde_json::Map::from_iter([("type".to_owned(), Value::Array(schema_types.to_vec()))]);
if let Some(values) = object.get("enum") {
let values = values.as_array().ok_or(ProviderError::Rejected)?;
if values.is_empty()
|| values.len() > 100
|| values.iter().any(|value| {
!codex_scalar_union_accepts(value, &unique_types)
|| value
.as_str()
.is_some_and(|value| value.is_empty() || value.len() > 200)
})
{
return Err(ProviderError::Rejected);
}
projected.insert("enum".to_owned(), Value::Array(values.clone()));
}
let projected_description = projected_codex_description(description, &constraint_notes)?;
if !projected_description.is_empty() {
projected.insert(
"description".to_owned(),
Value::String(projected_description),
);
}
Ok(Value::Object(projected))
}

fn codex_scalar_union_accepts(value: &Value, schema_types: &BTreeSet<&str>) -> bool {
match value {
Value::Null => schema_types.contains("null"),
Value::Bool(_) => schema_types.contains("boolean"),
Value::String(_) => schema_types.contains("string"),
Value::Number(number) => {
schema_types.contains("number")
|| (schema_types.contains("integer")
&& (number.as_i64().is_some() || number.as_u64().is_some()))
}
Value::Array(_) | Value::Object(_) => false,
}
}

fn validate_codex_schema_description(description: &str) -> Result<(), ProviderError> {
if description.len() > 2_000
|| description
Expand Down Expand Up @@ -6518,6 +6644,79 @@ pub(crate) mod tests {
assert_ne!(projected.fingerprints, substituted_projection.fingerprints);
}

#[test]
fn fixed_broker_definitions_project_their_nullable_scalar_unions_for_codex() {
let definitions = crate::capability_broker_definitions(&"a".repeat(64))
.expect("fixed broker definitions should compile");
assert_eq!(definitions.len(), 3);
let projected = project_codex_dynamic_tools(&definitions)
.expect("every crate-authored fixed broker schema should project");
assert_eq!(projected.protocol_values.len(), 3);

let discover = projected
.protocol_values
.iter()
.find(|tool| tool.get("name") == Some(&json!("graphql_capabilities_discover")))
.expect("discover projection should exist");
assert_eq!(
discover.pointer("/inputSchema/properties/namespace/type"),
Some(&json!(["string", "null"]))
);
assert_eq!(
discover.pointer("/inputSchema/properties/kind/enum"),
Some(&json!(["generated_query", null]))
);

let execute = projected
.protocol_values
.iter()
.find(|tool| tool.get("name") == Some(&json!("graphql_capabilities_execute")))
.expect("execute projection should exist");
assert_eq!(
execute.pointer("/inputSchema/properties/arguments/items/properties/value/type"),
Some(&json!(["string", "integer", "number", "boolean", "null"]))
);
assert_eq!(
execute.pointer("/inputSchema/properties/maximumItems/type"),
Some(&json!(["integer", "null"]))
);
assert!(
execute
.pointer("/inputSchema/properties/maximumItems/description")
.and_then(Value::as_str)
.is_some_and(|description| {
description.contains("minimum 1") && description.contains("maximum 10000")
})
);
for tool in &projected.protocol_values {
jsonschema::validator_for(&tool["inputSchema"])
.expect("projected fixed broker schema should remain valid JSON Schema");
}
}

#[test]
fn codex_nullable_scalar_union_projection_rejects_widened_or_malformed_shapes() {
for malformed in [
json!({"type": ["null"]}),
json!({"type": ["string", "integer"]}),
json!({"type": ["string", "string", "null"]}),
json!({"type": ["object", "null"]}),
json!({
"type": ["string", "null"],
"properties": {},
"additionalProperties": false
}),
json!({"type": ["boolean", "null"], "minimum": 1}),
json!({"type": ["integer", "null"], "enum": ["not-an-integer", null]}),
json!({"type": ["string", "null"], "enum": [false, null]}),
] {
assert!(matches!(
project_codex_schema_node(&malformed, 0),
Err(ProviderError::Rejected)
));
}
}

#[test]
fn finite_relational_query_plan_projects_without_generic_schema_passthrough() {
let tool = ModelToolDefinition {
Expand Down Expand Up @@ -6601,7 +6800,7 @@ pub(crate) mod tests {
.and_then(Value::as_str)
.is_some_and(|description| description.contains("maximum 25"))
);
assert!(schema.to_string().find("anyOf").is_none());
assert!(!schema.to_string().contains("anyOf"));
}

fn named_semantic_type(name: &str, nullable: bool) -> GraphqlSemanticTypeRef {
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/workspace-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ changes.
| Package | Version | Path | Default features | Direct internal dependencies |
| --- | --- | --- | --- | --- |
| `graphql-orm` | `0.23.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) |
| `graphql-orm-ai` | `0.88.0` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` |
| `graphql-orm-ai` | `0.88.1` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` |
| `graphql-orm-ai-tool-profiles` | `0.9.0` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) |
| `graphql-orm-backup` | `0.7.1` | `crates/graphql-orm-backup` | `local` | `graphql-orm` (optional), `graphql-orm-storage` |
| `graphql-orm-macros` | `0.23.0` | `crates/graphql-orm-macros` | `sqlite` | none |
Expand Down
Loading