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
4 changes: 2 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
17 changes: 17 additions & 0 deletions crates/graphql-orm-ai-tool-profiles/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai-tool-profiles/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm-ai-tool-profiles"
version = "0.10.0"
version = "0.10.1"
edition = "2024"
authors = ["Toby Martin <toby@dastari.net>"]
description = "Backend-neutral GraphQL AI tool profile compiler and manifest contracts"
Expand Down
18 changes: 18 additions & 0 deletions crates/graphql-orm-ai-tool-profiles/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions crates/graphql-orm-ai-tool-profiles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<reviewed-full-40-character-commit-sha>", version = "0.10.0" }
graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.10.1" }
serde_json = "1"
```

Expand Down Expand Up @@ -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.
Expand Down
129 changes: 121 additions & 8 deletions crates/graphql-orm-ai-tool-profiles/src/query_plans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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",
));
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(());
Expand Down Expand Up @@ -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"},
Expand All @@ -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();
Expand Down
20 changes: 20 additions & 0 deletions crates/graphql-orm-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
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.95.1"
version = "0.95.2"
edition = "2024"
authors = ["Toby Martin <toby@dastari.net>"]
description = "Project-agnostic AI agent runtime for graphql-orm applications"
Expand Down
14 changes: 14 additions & 0 deletions crates/graphql-orm-ai/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 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.95.1", 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.95.2", default-features = false, features = ["sqlite"] }
```

Exactly one persistence backend is required: `sqlite` (default), `postgres`,
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-ai/docs/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
12 changes: 8 additions & 4 deletions crates/graphql-orm-ai/src/providers/codex_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/workspace-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down