diff --git a/Cargo.lock b/Cargo.lock index 3a89508..34e52a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.94.0" +version = "0.95.0" dependencies = [ "agql-auth", "async-graphql", diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index c2e2362..3021c08 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -18,6 +18,28 @@ 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.0] - 2026-08-25 + +Persistent schema module: **0.63.0** (unchanged from 0.94.0). + +### Fixed + +- Initial provider-retained dynamic application-tool plans now admit + independently governed provider built-ins, including hosted search, instead + of rejecting the combined plan before provider transport. + +### Security + +- Coexistence does not grant a built-in capability. The provider call still + requires a structurally valid built-in request, an exact provider capability, + resolved-rule permission, separately authorized egress, bounded call and + budget capacity, and authoritative lifecycle usage accounting. Attachments, + tool-result input, reasoning summaries, output schemas, and continuations + remain prohibited by the initial dynamic-tool constructor. + +There is no database, data, table, column, index, constraint, backfill, +protected-payload, GraphQL SDL, backup, or restore migration. + ## [0.94.0] - 2026-08-23 Persistent schema module: **0.63.0** (unchanged from 0.93.2). diff --git a/crates/graphql-orm-ai/Cargo.toml b/crates/graphql-orm-ai/Cargo.toml index de3aead..7e1eca2 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.94.0" +version = "0.95.0" 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 2464c4a..b37af6c 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.94.0 to 0.95.0: retained dynamic tools with governed built-ins + +Adopt `graphql-orm-ai` 0.95.0 from one reviewed full monorepo revision. Hosts +may now use `AiReadOnlyAgentTurnPlan::new_experimental_dynamic_tools` for an +initial provider-retained request that combines registered application tools +with provider built-ins already validated by `AiProviderCallPlan`. + +No new authority is implied by coexistence. Continue supplying each built-in's +provider capability, resolved-rule permission, separate egress manifest, +bounded call ceiling, atomic budget capacity, and lifecycle usage accounting. +Existing dynamic-tool requests without built-ins are unchanged. The AI schema +module remains **0.63.0**; there is no database, data, GraphQL SDL, +protected-payload, backup, or restore migration. + ## 0.93.2 to 0.94.0: shape-aware capability discovery Adopt `graphql-orm-ai` 0.94.0 and `graphql-orm-ai-tool-profiles` 0.10.0 from one diff --git a/crates/graphql-orm-ai/README.md b/crates/graphql-orm-ai/README.md index c70fe2a..5b2efa5 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.94.0", default-features = false, features = ["sqlite"] } +graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.95.0", 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 0b53227..4862b7b 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.94.0` with AI schema module +`graphql-orm-ai` is at crate version `0.95.0` 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/docs/provider-sessions-and-hosted-activity.md b/crates/graphql-orm-ai/docs/provider-sessions-and-hosted-activity.md index 51cb848..75b20a3 100644 --- a/crates/graphql-orm-ai/docs/provider-sessions-and-hosted-activity.md +++ b/crates/graphql-orm-ai/docs/provider-sessions-and-hosted-activity.md @@ -320,6 +320,14 @@ disclosure- and egress-approved result is returned to app-server. Unknown, duplicate, stale, over-limit, changed-policy, or incomplete calls poison the turn and make a retained cursor cleanup-only. +A retained dynamic-tool turn may also carry provider built-ins that already +passed the ordinary provider-call contract. Coexistence itself grants nothing: +each built-in still requires exact provider capability and resolved-rule +permission, its own authorized egress transfer, a request call ceiling, atomic +budget capacity, and authoritative start/completion accounting. The initial +dynamic-tool boundary continues to reject attachments, tool-result input, +reasoning summaries, output schemas, and continuations. + Owning subgraphs compile generated or custom profiles into a canonical manifest and register it in `AiToolCatalog`. Build the provider definition with `AiToolCatalog::read_only_model_definition`; do not copy the description, diff --git a/crates/graphql-orm-ai/src/orm_coordinator.rs b/crates/graphql-orm-ai/src/orm_coordinator.rs index 6f5fa5b..5b7e74a 100644 --- a/crates/graphql-orm-ai/src/orm_coordinator.rs +++ b/crates/graphql-orm-ai/src/orm_coordinator.rs @@ -154,8 +154,11 @@ impl AiReadOnlyAgentTurnPlan { /// /// Returns [`AiError::InvalidInput`] unless the plan contains at least one /// application tool, is an initial provider-retained request without - /// built-ins, attachments, result input, reasoning summaries, or an output - /// schema, is exactly rule-bound, and has a valid result-egress route. + /// attachments, result input, reasoning summaries, or an output schema, + /// is exactly rule-bound, and has a valid result-egress route. Validated + /// provider built-ins may coexist with the application tools; their + /// capability, egress, budget, and usage proofs remain independently + /// enforced by the provider-call plan and executor. pub fn new_experimental_dynamic_tools( provider_call: AiProviderCallPlan, result_egress_route: AiToolResultEgressRoute, @@ -3833,6 +3836,26 @@ mod tests { )); } + #[test] + fn retained_dynamic_tool_factory_accepts_validated_provider_builtins() { + let lease = AiRunLease::test_running(principal_reference()); + let scope = test_scope(); + let provider_call = AiProviderCallPlan::test_dynamic_builtin_plan(&lease, scope.clone()); + + let plan = AiReadOnlyAgentTurnPlan::new_experimental_dynamic_tools( + provider_call, + test_route(), + test_rules(scope), + false, + ) + .expect("a validated hosted built-in may coexist with retained dynamic tools"); + let (_, _, _, mode, _, _, _, _) = plan.into_parts(); + assert!(matches!( + mode, + AiReadOnlyAgentTurnMode::ExperimentalDynamicTools(_) + )); + } + #[tokio::test] async fn pre_transport_budget_denial_fails_cleanly_instead_of_requiring_recovery() { let lease = AiRunLease::test_running(principal_reference()); diff --git a/crates/graphql-orm-ai/src/provider_calls.rs b/crates/graphql-orm-ai/src/provider_calls.rs index 6558f01..39d2e08 100644 --- a/crates/graphql-orm-ai/src/provider_calls.rs +++ b/crates/graphql-orm-ai/src/provider_calls.rs @@ -1124,7 +1124,6 @@ impl AiProviderCallPlan { !self.request.tools.is_empty() && self.request.continuation.is_none() && self.request.continuation_mode == ModelContinuationMode::ProviderRetained - && self.request.builtin_tools.is_empty() && self.request.reasoning_summary.maximum_bytes().is_none() && self.request.output_schema.is_none() && !self.request.input.iter().any(|block| { @@ -1479,6 +1478,17 @@ impl AiProviderCallPlan { plan } + #[cfg(test)] + pub(crate) fn test_dynamic_builtin_plan(lease: &AiRunLease, scope: crate::AiScope) -> Self { + let mut plan = Self::test_plan(lease, scope, false); + plan.request.builtin_tools = vec![ModelBuiltinTool::WebSearch { + domains: crate::ModelWebSearchDomainPolicy::PublicWeb, + }]; + plan.request.maximum_builtin_tool_calls = Some(2); + plan.budget.estimate.tool_units = 2; + plan + } + #[cfg(test)] pub(crate) fn test_chat_continuation_plan(lease: &AiRunLease, scope: crate::AiScope) -> Self { let mut plan = Self::test_chat_plan(lease, scope); @@ -5669,6 +5679,35 @@ mod tests { .expect("both custom-tool capabilities should permit the estimate"); } + #[tokio::test] + async fn retained_dynamic_tools_and_governed_web_search_share_one_plan() { + let fixture = fixture(Vec::new()).await; + let plan = dynamic_web_search_plan(&fixture, 2); + let resolution = AiAgentRuleResolution::new( + test_rules(fixture.scope.clone()), + OffsetDateTime::now_utc(), + ) + .expect("test rules should resolve"); + plan.project_rule_usage(&resolution, AiRuleRunUsage::default(), false) + .expect("the combined tool and hosted-search estimate should fit current rules"); + + crate::AiReadOnlyAgentTurnPlan::new_experimental_dynamic_tools( + plan, + crate::AiToolResultEgressRoute::new( + "mock-profile", + "local-mock", + AiDestinationTrust::ManagedProvider, + "test_inference", + "none", + "egress-v1", + ) + .expect("test result-egress route should validate"), + resolution.rules().clone(), + false, + ) + .expect("governed hosted search should coexist with retained dynamic tools"); + } + fn plan(fixture: &Fixture) -> AiProviderCallPlan { let request = ModelRequest { model: "mock-model".to_owned(), @@ -8297,6 +8336,34 @@ mod tests { .expect("registered enabled read-only tool plan should validate") } + fn dynamic_web_search_plan( + fixture: &Fixture, + maximum_builtin_tool_calls: u64, + ) -> AiProviderCallPlan { + let mut base = tool_plan(fixture); + base.request.builtin_tools = vec![ModelBuiltinTool::WebSearch { + domains: crate::ModelWebSearchDomainPolicy::AllowedDomains { + domains: vec!["example.com".to_owned()], + }, + }]; + base.request.maximum_builtin_tool_calls = Some(maximum_builtin_tool_calls); + base.budget.estimate.tool_units = maximum_builtin_tool_calls; + base.transfers[0].estimated_bytes = base.request.conservative_egress_bytes(); + let mut web_search_manifest = base.transfers[0].clone(); + web_search_manifest.capability = AiEgressCapability::WebSearch; + base.transfers.push(web_search_manifest); + AiProviderCallPlan::new_with_tools( + base.provider_kind, + base.request, + base.budget, + base.transfers, + base.correlation_id, + fixture.runtime.tool_catalog(), + &static_read_policy(fixture), + ) + .expect("dynamic-tool and web-search provider plan should validate") + } + fn static_read_policy(fixture: &Fixture) -> AiToolPolicySet { let descriptor = fixture .runtime diff --git a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock index 688e4e7..13a9a93 100644 --- a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock +++ b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock @@ -1329,7 +1329,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.94.0" +version = "0.95.0" dependencies = [ "agql-auth", "async-graphql", diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index 6e46ec8..4294d21 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -19,7 +19,7 @@ changes. | Package | Version | Path | Default features | Direct internal dependencies | | --- | --- | --- | --- | --- | | `graphql-orm` | `0.26.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | -| `graphql-orm-ai` | `0.94.0` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | +| `graphql-orm-ai` | `0.95.0` | `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-backup` | `0.7.1` | `crates/graphql-orm-backup` | `local` | `graphql-orm` (optional), `graphql-orm-storage` | | `graphql-orm-macros` | `0.26.0` | `crates/graphql-orm-macros` | `sqlite` | none |