diff --git a/Cargo.lock b/Cargo.lock index ae2512e..a947ad1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.87.0" +version = "0.88.0" dependencies = [ "agql-auth", "async-graphql", diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index 7ecef46..0d39b98 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -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.0] - 2026-08-22 + +Persistent schema module: **0.63.0** (unchanged from 0.87.0). + +### Changed + +- The closed Codex dynamic-tool launch profile omits only the process-level + `--disable code_mode_host` argument, which Codex 0.148.0 was measured to + require for direct `dynamicToolCall` delivery to GPT-5.6 Luna. + +### Security + +- Code Mode, Code Mode-only routing, shell, files, MCP, browser, hosted web + search, and every other native surface remain disabled. The per-thread + configuration still sets `features.code_mode_host=false`, and the protocol + actor rejects command, file, MCP, collaboration, search, and image items at + both lifecycle boundaries. + +There is no schema, data, protected-payload, GraphQL SDL, backup or restore +migration in this release. + ## [0.87.0] - 2026-08-22 Persistent schema module: **0.63.0** (unchanged from 0.86.0). diff --git a/crates/graphql-orm-ai/Cargo.toml b/crates/graphql-orm-ai/Cargo.toml index e91d6d8..4fb2018 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.87.0" +version = "0.88.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 0b6acf4..b89476a 100644 --- a/crates/graphql-orm-ai/MIGRATION.md +++ b/crates/graphql-orm-ai/MIGRATION.md @@ -19,6 +19,29 @@ 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.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 +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 must continue applying +`AiCodexAppServerLaunchProfile::codex_arguments()` unchanged. On Codex 0.148.0, +the profile now omits only `--disable code_mode_host`: an otherwise identical +GPT-5.6 Luna probe completed without calling its offered direct tool when that +argument was present, and emitted `dynamicToolCall` / `item/tool/call` when it +was absent. Do not infer that Code Mode or another native surface is admitted. +The actor still sends `features.code_mode_host=false`, +`features.code_mode=false`, `features.code_mode_only=false`, and every other +closed feature setting per thread; the process sandbox and protocol actor +continue to deny shell, file, MCP, browser, hosted-search, collaboration, +image, and arbitrary server-request items. + +Before changing Codex versions, run a retained direct-tool readiness probe and +the negative native-item lifecycle tests. A non-dynamic route or any native +item must fail readiness rather than falling back to Code Mode or execution. + ## 0.86.0 to 0.87.0: retained dynamic-tool readiness input Adopt `graphql-orm-ai` 0.87.0 from one reviewed full monorepo revision. The AI diff --git a/crates/graphql-orm-ai/README.md b/crates/graphql-orm-ai/README.md index d2eefab..419d215 100644 --- a/crates/graphql-orm-ai/README.md +++ b/crates/graphql-orm-ai/README.md @@ -149,6 +149,16 @@ message, tool call, or checkpoint. This discard guarantee is version-observed for `codex-cli 0.148.0` with `gpt-5.4`; reverify it before upgrading Codex. Any missing proof continues through the disclosed cleanup-and-rebind path. +The retained dynamic-tool launch profile is version-observed on Codex 0.148.0. +It disables Code Mode, Code Mode-only routing, shell, files, MCP, browser, +hosted search, and every other native item surface. Its sole process-level +exception is `code_mode_host`: `--disable code_mode_host` suppresses direct +`dynamicToolCall` delivery on that Codex version, so the launch arguments omit +only that flag while the per-thread configuration still sets the feature +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. + See the [session reliability adoption contract](docs/session-reliability-adoption.md). ## Features and capability boundary 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 6647949..879f3a0 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 @@ -177,6 +177,17 @@ collaboration, images, browser/computer use, and hosted search. This is defense in depth: the process sandbox remains authoritative if a provider version ignores a feature toggle. +The sole process-level exception is measured on Codex 0.148.0: adding +`--disable code_mode_host` to this otherwise identical profile made a retained +GPT-5.6 Luna turn complete without issuing its offered tool, while omitting +that one argument produced the direct `dynamicToolCall` / `item/tool/call`. +`codex_arguments()` therefore omits only that disable. The actor still sends +`features.code_mode_host=false`, `features.code_mode=false`, and +`features.code_mode_only=false` per thread; shell, file, MCP, browser, web, and +every other native item remain unavailable and are rejected by the protocol +actor if emitted. Re-run the direct-tool readiness probe and negative native- +item suite before adopting another Codex version. + Only a reviewed `Direct` model-tool declaration can construct this profile. Codex models declared `CodeMode` or `CodeModeOnly` are rejected rather than silently losing dynamic tools or requiring a native Code Mode host. Such a 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 fd50029..1aa0625 100644 --- a/crates/graphql-orm-ai/src/providers/codex_app_server.rs +++ b/crates/graphql-orm-ai/src/providers/codex_app_server.rs @@ -60,6 +60,7 @@ const OPTED_OUT_NOTIFICATION_METHODS: [&str; 5] = [ const REMOTE_CONTROL_STATUS_CHANGED: &str = "remoteControl/status/changed"; const RUNTIME_WARNING: &str = "warning"; const THREAD_TOKEN_USAGE_UPDATED: &str = "thread/tokenUsage/updated"; +const CODE_MODE_HOST_FEATURE: &str = "code_mode_host"; const DYNAMIC_TOOLS_ONLY_DISABLED_FEATURES: &[&str] = &[ "apps", @@ -274,15 +275,27 @@ impl AiCodexAppServerLaunchProfile { /// /// The dynamic profile deliberately disables every native execution, /// browser, hosted-search, connector, collaboration, image, plugin, and - /// interactive tool feature it relies on being absent. The factory must - /// also clear the environment, use a private configuration home containing - /// no project configuration or MCP servers, use an empty working - /// directory, and apply its reviewed external sandbox. + /// interactive tool feature it relies on being absent. Codex 0.148.0 is + /// the measured exception for the internal `code_mode_host` process gate: + /// disabling that one gate suppresses direct `dynamicToolCall` delivery, + /// so it remains available at process launch while Code Mode, Code + /// Mode-only routing, and the host itself remain false in the closed + /// per-thread configuration. The factory must also clear the environment, + /// use a private configuration home containing no project configuration + /// or MCP servers, use an empty working directory, and apply its reviewed + /// external sandbox. #[must_use] pub fn codex_arguments(self) -> Vec<&'static str> { let mut arguments = vec!["app-server", "--stdio", "--strict-config"]; if self.supports_experimental_dynamic_tools() { for feature in DYNAMIC_TOOLS_ONLY_DISABLED_FEATURES { + // Codex 0.148.0 suppresses direct `dynamicToolCall` delivery + // when this process-level feature gate is disabled. The + // per-thread configuration below still sets it false beside + // Code Mode and every model-native tool surface. + if *feature == CODE_MODE_HOST_FEATURE { + continue; + } arguments.extend(["--disable", *feature]); } } @@ -7426,6 +7439,15 @@ pub(crate) mod tests { ); assert!(!arguments.contains(&"--enable")); for feature in DYNAMIC_TOOLS_ONLY_DISABLED_FEATURES { + if *feature == CODE_MODE_HOST_FEATURE { + assert!( + !arguments + .windows(2) + .any(|pair| pair == ["--disable", *feature]), + "Codex 0.148.0 requires the process-level {feature} gate for direct dynamicToolCall delivery" + ); + continue; + } assert!( arguments .windows(2) @@ -10359,6 +10381,43 @@ pub(crate) mod tests { )); } + #[test] + fn direct_dynamic_tool_turn_rejects_every_native_item_surface() { + let input = AiCodexAppServerTurnInput::try_from_dynamic_request(dynamic_model_request()) + .expect("dynamic request should convert"); + for item_type in [ + "commandExecution", + "fileChange", + "mcpToolCall", + "collabToolCall", + "webSearch", + "imageView", + ] { + for (method, timestamp) in [ + ("item/started", "startedAtMs"), + ("item/completed", "completedAtMs"), + ] { + let mut actor = initialized_protocol_actor(); + let thread_id = start_bound_dynamic_thread(&mut actor, input.tools()); + start_bound_dynamic_turn(&mut actor, &thread_id, &input, "turn-native-1", 3); + let mut params = json!({ + "item": {"type": item_type, "id": "native-item-1"}, + "threadId": thread_id, + "turnId": "turn-native-1", + }); + params + .as_object_mut() + .expect("native item params should be an object") + .insert(timestamp.to_owned(), json!(1)); + let frame = lifecycle_notification(method, params); + assert!( + matches!(actor.accept(&frame), Err(ProviderError::Rejected)), + "native item {item_type} at {method} must fail the direct dynamic-tool turn" + ); + } + } + } + #[test] fn protocol_accepts_only_correlated_responses_and_allowlisted_notifications() { let mut unbound = @@ -10608,6 +10667,13 @@ pub(crate) mod tests { assert!(start.contains("\"sandbox\":\"read-only\"")); let start_value: Value = serde_json::from_str(start.trim()).expect("dynamic start should remain valid JSON"); + for feature in DYNAMIC_TOOLS_ONLY_DISABLED_FEATURES { + assert_eq!( + start_value.pointer(&format!("/params/config/features.{feature}")), + Some(&Value::Bool(false)), + "thread config must disable native feature {feature}" + ); + } assert_eq!( start_value.pointer("/params/environments"), Some(&json!([])) @@ -10624,6 +10690,14 @@ pub(crate) mod tests { start_value.pointer("/params/config/features.code_mode"), Some(&Value::Bool(false)) ); + assert_eq!( + start_value.pointer("/params/config/features.code_mode_host"), + Some(&Value::Bool(false)) + ); + assert_eq!( + start_value.pointer("/params/config/features.code_mode_only"), + Some(&Value::Bool(false)) + ); assert_eq!( start_value.pointer("/params/config/features.apps"), Some(&Value::Bool(false)) diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index 6ab0a4b..9e4f1be 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.23.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | -| `graphql-orm-ai` | `0.87.0` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | +| `graphql-orm-ai` | `0.88.0` | `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 |