diff --git a/Cargo.lock b/Cargo.lock index 1e39c8c..3d25e5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.95.1" +version = "0.95.2" dependencies = [ "agql-auth", "async-graphql", @@ -3136,7 +3136,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai-tool-profiles" -version = "0.10.0" +version = "0.10.1" dependencies = [ "async-graphql", "async-graphql-parser", diff --git a/Cargo.toml b/Cargo.toml index b0cf747..0e4837f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ futures = "0.3" getrandom = "0.3" graphql-composition = "=0.12.2" graphql-orm = { path = "crates/graphql-orm", version = "0.27.0", default-features = false } -graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.10.0" } +graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.10.1" } graphql-orm-backup = { path = "crates/graphql-orm-backup", version = "0.7.1", default-features = false } graphql-orm-operation-catalog = { path = "crates/graphql-orm-operation-catalog", version = "0.3.0" } graphql-orm-router-protocol = { path = "crates/graphql-orm-router-protocol", version = "0.2.1" } diff --git a/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md b/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md index 961be6f..70dcac8 100644 --- a/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md +++ b/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md @@ -10,6 +10,23 @@ supersedes: [] # Changelog +## [0.10.1] - 2026-08-26 + +### Fixed + +- Compact query, mutation, and subscription schemas now encode their exact + scalar selection allow-list as one string enum instead of repeating one + described `const` schema for every reachable path. Wide relationship graphs + therefore remain within the provider-schema byte contract without removing + public paths or weakening plan validation. + +### Security + +- Selection paths remain a closed compiler-owned allow-list. Unknown, hidden, + secret, `NeverExport`, stale, cyclic, over-depth, over-cardinality, and + over-budget plans continue to fail closed; canonical discovery retains the + public field descriptions omitted from the repeated provider schema. + ## [0.10.0] - 2026-08-23 ### Added diff --git a/crates/graphql-orm-ai-tool-profiles/Cargo.toml b/crates/graphql-orm-ai-tool-profiles/Cargo.toml index 763b1a1..dd29b53 100644 --- a/crates/graphql-orm-ai-tool-profiles/Cargo.toml +++ b/crates/graphql-orm-ai-tool-profiles/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-orm-ai-tool-profiles" -version = "0.10.0" +version = "0.10.1" edition = "2024" authors = ["Toby Martin "] description = "Backend-neutral GraphQL AI tool profile compiler and manifest contracts" diff --git a/crates/graphql-orm-ai-tool-profiles/MIGRATION.md b/crates/graphql-orm-ai-tool-profiles/MIGRATION.md index a43bf09..f8791fa 100644 --- a/crates/graphql-orm-ai-tool-profiles/MIGRATION.md +++ b/crates/graphql-orm-ai-tool-profiles/MIGRATION.md @@ -10,6 +10,24 @@ supersedes: [] # Migration Guide +## 0.10.0 to 0.10.1: provider-sized selection allow-lists + +Adopt `graphql-orm-ai-tool-profiles` 0.10.1 from one reviewed full monorepo +revision. Recompile generated capability catalogues and refresh exact +capability/provider-session bindings because the compact JSON Schema and its +derived capability fingerprints change. + +The compact plan JSON is unchanged: callers still submit a `selections` array +of exact public scalar paths plus typed relationship arguments and bounds. The +schema now represents those paths as one closed string enum instead of a +separate described `const` alternative per path. Public descriptions remain +available through the canonical capability index, and relationship argument +descriptions remain in the loaded schema. + +There is no database, data, GraphQL SDL, protected-content, backup, restore, or +persistent AI schema-module migration. Resolver authorization, target policy, +disclosure validation, and result budgets remain authoritative. + ## 0.9.0 to 0.10.0: shape-aware discovery and record-cost metadata Adopt `graphql-orm-ai-tool-profiles` 0.10.0 with `graphql-orm-ai` 0.94.0 from diff --git a/crates/graphql-orm-ai-tool-profiles/README.md b/crates/graphql-orm-ai-tool-profiles/README.md index 1e997a2..0bf6ddc 100644 --- a/crates/graphql-orm-ai-tool-profiles/README.md +++ b/crates/graphql-orm-ai-tool-profiles/README.md @@ -24,7 +24,7 @@ are separate runtime decisions and must remain default-deny. ```toml [dependencies] -graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.10.0" } +graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.10.1" } serde_json = "1" ``` @@ -104,8 +104,12 @@ variables schema, disclosure shape, limits, and plan fingerprint. Registration is discovery only; a fresh target/current-principal policy and the ordinary resolver remain authoritative at execution. -The compact selection schema is a finite list of public paths, so adding deep -relationships does not recursively duplicate the entire nested field map. +The compact selection schema is one finite string enum of public paths, so +adding deep relationships does not recursively duplicate either the nested +field map or scalar descriptions at every reachable path. Public scalar and +relationship descriptions remain in the canonical discovery index; typed +relationship arguments retain their adjacent descriptions in the loaded +planning schema. Generated entity `WhereInput` objects still omit recursive `And`/`Or`/`Not` connectives; all non-recursive typed filter fields remain available. Handwritten recursive inputs fail readiness rather than being approximated. diff --git a/crates/graphql-orm-ai-tool-profiles/src/query_plans.rs b/crates/graphql-orm-ai-tool-profiles/src/query_plans.rs index a785481..873aa42 100644 --- a/crates/graphql-orm-ai-tool-profiles/src/query_plans.rs +++ b/crates/graphql-orm-ai-tool-profiles/src/query_plans.rs @@ -2795,7 +2795,7 @@ fn build_compact_plan_schema( &mut Vec::new(), inject_root_bound, )?; - let mut selection_alternatives = Vec::new(); + let mut selection_paths = Vec::new(); let mut relationship_schemas = Vec::new(); if let QueryOutput::Entity { entity, .. } = output { collect_compact_selection_paths( @@ -2806,10 +2806,10 @@ fn build_compact_plan_schema( 0, &mut vec![entity.clone()], "", - &mut selection_alternatives, + &mut selection_paths, &mut relationship_schemas, )?; - if selection_alternatives.is_empty() { + if selection_paths.is_empty() { return Err(configuration_error( "query entity has no compact exportable selections", )); @@ -2841,7 +2841,7 @@ fn build_compact_plan_schema( json!({ "type": "array", "description": "Explicit public scalar selection paths. No select-all behavior.", - "items": { "oneOf": selection_alternatives }, + "items": { "type": "string", "enum": selection_paths }, "minItems": 1, "maxItems": i64::from(limits.maximum_selected_fields), "uniqueItems": true, @@ -2929,10 +2929,7 @@ fn collect_compact_selection_paths( { schema.object_field(entity_name, &field.field_name)?; let path = join_selection_path(prefix, &field.field_name); - selections.push(json!({ - "const": path, - "description": field.description, - })); + selections.push(Value::String(path)); } if depth + 1 >= limits.maximum_depth { return Ok(()); @@ -4974,6 +4971,24 @@ mod tests { assert!(compact_schema.contains("children.label")); assert!(compact_schema.contains("Bounded related child records.")); assert!(!compact_schema.contains("credential")); + assert_eq!( + capability + .compact_argument_schema() + .pointer("/properties/selections/items/type"), + Some(&json!("string")) + ); + assert!( + capability + .compact_argument_schema() + .pointer("/properties/selections/items/enum") + .is_some() + ); + assert!( + capability + .compact_argument_schema() + .pointer("/properties/selections/items/oneOf") + .is_none() + ); let compiled = capability .compile_compact(json!({ "arguments": {"id": "parent-1"}, @@ -4991,6 +5006,104 @@ mod tests { assert_eq!(compiled.variables()["v1"]["limit"], json!(2)); } + #[test] + fn compact_selection_allowlist_stays_within_provider_limits_for_wide_graphs() { + const LEVELS: usize = 4; + const RELATIONSHIPS_PER_LEVEL: usize = 5; + const SCALARS_PER_ENTITY: usize = 16; + + let mut sdl = String::from("schema { query: Query }\ntype Query { ReadRoot: Level0! }\n"); + let mut entities = Vec::new(); + for level in 0..LEVELS { + let entity_name = format!("Level{level}"); + sdl.push_str(&format!("type {entity_name} {{\n")); + let mut fields = Vec::new(); + for scalar in 0..SCALARS_PER_ENTITY { + let name = format!("value{scalar}"); + sdl.push_str(&format!(" {name}: String!\n")); + let mut field = scalar_field( + &name, + GraphqlSemanticClassification::Internal, + GraphqlSemanticExport::Exportable, + ); + field.description = format!("Public {name} value. {}", "x".repeat(900)); + fields.push(field); + } + if level + 1 < LEVELS { + for relationship in 0..RELATIONSHIPS_PER_LEVEL { + let field_name = format!("related{relationship}"); + let target = format!("Level{}", level + 1); + sdl.push_str(&format!(" {field_name}: {target}!\n")); + fields.push(GraphqlSemanticFieldMetadata { + field_name, + description: "A related public record.".to_owned(), + type_ref: GraphqlSemanticTypeRef::named( + &target, + GraphqlSemanticTypeKind::Object, + false, + ), + selectable: true, + filter_operators: Vec::new(), + sortable: false, + groupable: false, + aggregate_operators: Vec::new(), + aggregate_value_kind: None, + relationship: Some(GraphqlSemanticRelationshipDescriptor { + target, + cardinality: GraphqlSemanticRelationshipCardinality::One, + arguments: Vec::new(), + collection_bound: None, + }), + classification: GraphqlSemanticClassification::Internal, + export: GraphqlSemanticExport::Exportable, + has_field_policy: false, + }); + } + } + sdl.push_str("}\n"); + entities.push(GraphqlEntitySemanticMetadata { + entity_name, + description: "One public graph level.".to_owned(), + default_classification: GraphqlSemanticClassification::Internal, + fields: fields.into_boxed_slice(), + }); + } + let operation = GraphqlSemanticOperationDescriptor::custom( + GraphqlOperationKind::Query, + "ReadRoot", + "Read the bounded public graph root.", + Vec::new(), + GraphqlSemanticTypeRef::named("Level0", GraphqlSemanticTypeKind::Object, false), + true, + ) + .expect("query semantics"); + let semantics = GraphqlSemanticCatalog::compose_with_custom( + entities, + &GraphqlOperationCatalog::compose(std::iter::empty()), + [operation], + ) + .expect("wide semantic graph"); + + let capabilities = AiGraphqlQueryCapabilityCatalog::compile( + "wide", + GraphqlExecutionTargetId::parse("wide.graphql").expect("target"), + &sdl, + &semantics, + AiGraphqlQueryCapabilityLimits::default(), + ) + .expect("wide graph must retain a provider-sized compact contract"); + let capability = capabilities.capabilities().next().expect("capability"); + let compact = serde_json::to_vec(capability.compact_argument_schema()).expect("schema"); + assert!(compact.len() < MAXIMUM_PROVIDER_SCHEMA_BYTES); + let paths = capability + .compact_argument_schema() + .pointer("/properties/selections/items/enum") + .and_then(Value::as_array) + .expect("closed selection allowlist"); + assert_eq!(paths.len(), 2_496); + assert!(paths.contains(&json!("related4.related4.related4.value15"))); + } + #[test] fn compact_cross_field_budget_failure_returns_a_correction_envelope() { let sdl = query_sdl(); diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index 921bb47..83eb894 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -18,6 +18,26 @@ 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.95.2] - 2026-08-26 + +Persistent schema module: **0.63.0** (unchanged from 0.95.1). + +### Changed + +- Fixed-broker generated-query definitions now expose exact public scalar + selection paths as one closed string enum. This preserves the existing + compact-plan JSON while keeping wide relationship graphs within provider + schema-size limits. + +### Security + +- The compiler-owned selection allowlist remains exact and closed. Unknown, + hidden, secret, non-exportable, cyclic, over-depth, over-cardinality, stale, + and over-budget plans remain rejected. + +There is no database, data, table, column, index, constraint, backfill, +protected-payload, GraphQL SDL, backup, or restore migration. + ## [0.95.1] - 2026-08-25 Persistent schema module: **0.63.0** (unchanged from 0.95.0). diff --git a/crates/graphql-orm-ai/Cargo.toml b/crates/graphql-orm-ai/Cargo.toml index 7859ad2..67e4a88 100644 --- a/crates/graphql-orm-ai/Cargo.toml +++ b/crates/graphql-orm-ai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-orm-ai" -version = "0.95.1" +version = "0.95.2" edition = "2024" authors = ["Toby Martin "] description = "Project-agnostic AI agent runtime for graphql-orm applications" diff --git a/crates/graphql-orm-ai/MIGRATION.md b/crates/graphql-orm-ai/MIGRATION.md index 09681a6..712167d 100644 --- a/crates/graphql-orm-ai/MIGRATION.md +++ b/crates/graphql-orm-ai/MIGRATION.md @@ -19,6 +19,20 @@ 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.95.1 to 0.95.2: compact closed selection allowlists + +Adopt `graphql-orm-ai` 0.95.2 and `graphql-orm-ai-tool-profiles` 0.10.1 from +one reviewed full monorepo revision. Existing compact plan payloads are +unchanged: `selections` remains an array of exact public scalar paths. Hosts +that persist provider tool-definition fingerprints must rebuild those bindings +because the equivalent closed schema is encoded as a string enum rather than +repeated `oneOf`/`const` objects. + +Selection, relationship, disclosure, authority, depth, cardinality, and result +budget enforcement are unchanged. The AI schema module remains **0.63.0**; +there is no database, data, GraphQL SDL, protected-payload, backup, or restore +migration. + ## 0.95.0 to 0.95.1: forward-compatible Codex web-search completion Adopt `graphql-orm-ai` 0.95.1 from one reviewed full monorepo revision. Hosts diff --git a/crates/graphql-orm-ai/README.md b/crates/graphql-orm-ai/README.md index 9ee9fc4..3a5aca9 100644 --- a/crates/graphql-orm-ai/README.md +++ b/crates/graphql-orm-ai/README.md @@ -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 = "", version = "0.95.1", default-features = false, features = ["sqlite"] } +graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.95.2", default-features = false, features = ["sqlite"] } ``` Exactly one persistence backend is required: `sqlite` (default), `postgres`, diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index 052e3fd..09bd3eb 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -10,7 +10,7 @@ supersedes: [] # Implementation Status -`graphql-orm-ai` is at crate version `0.95.1` with AI schema module +`graphql-orm-ai` is at crate version `0.95.2` with AI schema module `0.63.0`. It uses workspace `graphql-orm` `0.26.0`, backend-neutral `graphql-orm-ai-tool-profiles` `0.10.0`, and external `agql-auth` `0.18.0` at `527d15d28e3c295a6f6b5e6d74559a7aecdc1322`. diff --git a/crates/graphql-orm-ai/src/providers/codex_app_server.rs b/crates/graphql-orm-ai/src/providers/codex_app_server.rs index f34c1a8..14f0ca7 100644 --- a/crates/graphql-orm-ai/src/providers/codex_app_server.rs +++ b/crates/graphql-orm-ai/src/providers/codex_app_server.rs @@ -8126,11 +8126,15 @@ pub(crate) mod tests { assert!( definition .parameters - .pointer("/properties/selections/items/oneOf") + .pointer("/properties/selections/items/enum") .and_then(Value::as_array) - .is_some_and(|alternatives| alternatives - .iter() - .any(|alternative| alternative.get("const") == Some(&json!("children.id")))) + .is_some_and(|values| values.contains(&json!("children.id"))) + ); + assert!( + definition + .parameters + .pointer("/properties/selections/items/oneOf") + .is_none() ); let projected = project_codex_dynamic_tools(std::slice::from_ref(&definition)) diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index f56ef76..f7bbfdc 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -19,8 +19,8 @@ changes. | Package | Version | Path | Default features | Direct internal dependencies | | --- | --- | --- | --- | --- | | `graphql-orm` | `0.27.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | -| `graphql-orm-ai` | `0.95.1` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | -| `graphql-orm-ai-tool-profiles` | `0.10.0` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | +| `graphql-orm-ai` | `0.95.2` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | +| `graphql-orm-ai-tool-profiles` | `0.10.1` | `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.27.0` | `crates/graphql-orm-macros` | `sqlite` | none | | `graphql-orm-operation-catalog` | `0.3.0` | `crates/graphql-orm-operation-catalog` | none | `graphql-orm-router-protocol` (optional) |