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.

24 changes: 24 additions & 0 deletions crates/graphql-orm-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ 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.93.2] - 2026-08-23

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

### Fixed

- Failed application-tool previews now require the descriptor's explicit
browser-preview opt-in and the same fresh current host tool-policy
preauthorization as successful previews.
- Egress-denied rows are never browser-previewable, even if their persisted
authorization code resembles a public failure envelope.
- The safe failure-code browser mapping now has an exhaustive enum guard, so
adding a failure variant requires an explicit visibility decision.

### Security

- Failure previews remain current-owner, current-session/scope-authority,
exact-descriptor, public-classification, exact-envelope, and host-projection
gated. Revoked tool policy or missing browser opt-in now fails closed before
protected arguments or results can cross the browser boundary.

There is no database, data, table, column, index, constraint, backfill,
protected-payload, GraphQL SDL, backup, or restore migration.

## [0.93.1] - 2026-08-23

Persistent schema module: **0.63.0** (unchanged from 0.93.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.93.1"
version = "0.93.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.93.1 to 0.93.2: consistent current-policy browser preview gates

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

Hosts do not need to change an API implementation. Failed tool-call previews
now require an exact current descriptor with `browser_result_preview` enabled
and a fresh successful host tool-policy preauthorization, matching successful
preview behavior. Egress-denied rows no longer produce a browser preview.
Existing `AiToolResultPreviewAuthorizer` implementations that do not override
`authorize_and_project_arguments` continue returning `None` for arguments.

## 0.93.0 to 0.93.1: public web-search configuration export

Adopt `graphql-orm-ai` 0.93.1 from one reviewed full monorepo revision. The AI
Expand Down
12 changes: 7 additions & 5 deletions crates/graphql-orm-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: "graphql-orm-ai"
kind: reference
status: active
owner: graphql-orm-ai-maintainers
last_reviewed: 2026-08-16
last_reviewed: 2026-08-23
review_by: 2027-02-01
supersedes: []
---
Expand All @@ -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.93.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.93.2", default-features = false, features = ["sqlite"] }
```

Exactly one persistence backend is required: `sqlite` (default), `postgres`,
Expand Down Expand Up @@ -80,9 +80,11 @@ the compiled test-backed recipe and the missing reusable bootstrap API.
messages, durable watermark, active/recent runs, tool calls, provider
activity and retention reset state for race-free replay/live handoff.
- An owner-authorized tool-call preview rehydrates current authority before
returning host-projected arguments and either a disclosure-validated result
or the exact content-free safe failure envelope. Secret results never enter
this browser contract.
requiring the exact descriptor's browser-preview opt-in and freshly
reauthorizing current host tool policy. It then returns host-projected
arguments and either a disclosure-validated result or the exact
content-free safe failure envelope. Egress-denied and secret results never
enter this browser contract.
- Provider-neutral adapters plus deterministic network-free mocks.
- Optional provider profiles, attachments, skills, UI intents, rules, and
usage/pricing controls, each behind independent proof and policy boundaries.
Expand Down
165 changes: 84 additions & 81 deletions crates/graphql-orm-ai/src/orm_tool_result_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ impl AiToolCallResultPreviewService for OrmAiToolCallResultPreviewService {
.authorization_code
.as_deref()
.and_then(application_tool_failure_code)
.filter(|_| matches!(call.state.as_str(), "execution_failed" | "egress_denied"));
.filter(|_| call.state == "execution_failed");
if call.run_id != run.id
|| (!successful && safe_failure.is_none())
|| call.completed_at.is_none()
Expand All @@ -164,7 +164,12 @@ impl AiToolCallResultPreviewService for OrmAiToolCallResultPreviewService {
.runtime
.tool_catalog()
.descriptor(&tool_id)
.filter(|descriptor| descriptor.fingerprint == call.tool_fingerprint);
.filter(|descriptor| descriptor.fingerprint == call.tool_fingerprint)
.ok_or(AiError::Forbidden)?;
let preview_policy = match descriptor.browser_result_preview {
Some(policy) => policy,
None => return Ok(None),
};
let policy = self
.runtime
.content_protection_policy_resolver()
Expand All @@ -173,73 +178,6 @@ impl AiToolCallResultPreviewService for OrmAiToolCallResultPreviewService {
if !policy.ready || policy.scope != scope {
return Err(AiError::RuntimeNotReady);
}
if let Some(code) = safe_failure {
let classification = parse_classification(
call.result_classification
.as_deref()
.ok_or(AiError::PersistenceFailed)?,
)?;
if classification != DataClassification::Public {
return Err(AiError::PersistenceFailed);
}
let stored = self
.open(
&policy,
protection_context(call.id, "protected_result", &scope),
call.protected_result
.as_ref()
.ok_or(AiError::PersistenceFailed)?,
)
.await?;
let preview = extract_safe_failure(&stored, code)?;
let arguments = if let Some(descriptor) = descriptor
&& descriptor.browser_result_preview.is_some()
{
let stored_arguments = self
.open(
&policy,
protection_context(call.id, "protected_arguments", &scope),
call.protected_arguments
.as_ref()
.ok_or(AiError::PersistenceFailed)?,
)
.await?;
self.project_arguments(&current, &scope, descriptor, &stored_arguments)
.await?
} else {
None
};
return Ok(Some(AiToolCallResultPreviewView {
session_id: session.id,
run_id: run.id,
tool_call_id: call.id,
tool_id: call.tool_id,
classification: classification_name(classification).to_owned(),
arguments: arguments.map(async_graphql::Json),
preview: async_graphql::Json(preview),
}));
}
let descriptor = descriptor.ok_or(AiError::Forbidden)?;
let preview_policy = match descriptor.browser_result_preview {
Some(policy) => policy,
None => return Ok(None),
};
let disclosure = self
.runtime
.tool_catalog()
.disclosure_schema(&tool_id)
.filter(|schema| {
call.disclosure_schema_fingerprint.as_deref() == Some(schema.fingerprint.as_str())
})
.ok_or(AiError::Forbidden)?;
let classification = parse_classification(
call.result_classification
.as_deref()
.ok_or(AiError::PersistenceFailed)?,
)?;
if classification > preview_policy.maximum_classification {
return Ok(None);
}
let arguments = self
.open(
&policy,
Expand Down Expand Up @@ -289,6 +227,51 @@ impl AiToolCallResultPreviewService for OrmAiToolCallResultPreviewService {
&request.variables,
)
.await?;
if let Some(code) = safe_failure {
let classification = parse_classification(
call.result_classification
.as_deref()
.ok_or(AiError::PersistenceFailed)?,
)?;
if classification != DataClassification::Public {
return Err(AiError::PersistenceFailed);
}
let stored = self
.open(
&policy,
protection_context(call.id, "protected_result", &scope),
call.protected_result
.as_ref()
.ok_or(AiError::PersistenceFailed)?,
)
.await?;
let preview = extract_safe_failure(&stored, code)?;
return Ok(Some(AiToolCallResultPreviewView {
session_id: session.id,
run_id: run.id,
tool_call_id: call.id,
tool_id: call.tool_id,
classification: classification_name(classification).to_owned(),
arguments: arguments.map(async_graphql::Json),
preview: async_graphql::Json(preview),
}));
}
let disclosure = self
.runtime
.tool_catalog()
.disclosure_schema(&tool_id)
.filter(|schema| {
call.disclosure_schema_fingerprint.as_deref() == Some(schema.fingerprint.as_str())
})
.ok_or(AiError::Forbidden)?;
let classification = parse_classification(
call.result_classification
.as_deref()
.ok_or(AiError::PersistenceFailed)?,
)?;
if classification > preview_policy.maximum_classification {
return Ok(None);
}
let stored = self
.open(
&policy,
Expand Down Expand Up @@ -386,18 +369,38 @@ fn extract_exact_result(value: &serde_json::Value) -> Result<&serde_json::Value,
fn application_tool_failure_code(value: &str) -> Option<crate::AiApplicationToolFailureCode> {
use crate::AiApplicationToolFailureCode as Code;

match value {
"invalid_arguments" => Some(Code::InvalidArguments),
"selection_too_large" => Some(Code::SelectionTooLarge),
"relationship_depth_exceeded" => Some(Code::RelationshipDepthExceeded),
"result_budget_exceeded" => Some(Code::ResultBudgetExceeded),
"capability_stale" => Some(Code::CapabilityStale),
"authorization_denied" => Some(Code::AuthorizationDenied),
"temporarily_unavailable" => Some(Code::TemporarilyUnavailable),
"tool_unavailable" => Some(Code::ToolUnavailable),
"resolver_validation_failed" => Some(Code::ResolverValidationFailed),
"not_found" => Some(Code::NotFound),
_ => None,
let code = match value {
"invalid_arguments" => Code::InvalidArguments,
"selection_too_large" => Code::SelectionTooLarge,
"relationship_depth_exceeded" => Code::RelationshipDepthExceeded,
"result_budget_exceeded" => Code::ResultBudgetExceeded,
"capability_stale" => Code::CapabilityStale,
"authorization_denied" => Code::AuthorizationDenied,
"temporarily_unavailable" => Code::TemporarilyUnavailable,
"tool_unavailable" => Code::ToolUnavailable,
"resolver_validation_failed" => Code::ResolverValidationFailed,
"not_found" => Code::NotFound,
_ => return None,
};
Some(exhaustive_browser_failure_code(code))
}

const fn exhaustive_browser_failure_code(
code: crate::AiApplicationToolFailureCode,
) -> crate::AiApplicationToolFailureCode {
use crate::AiApplicationToolFailureCode as Code;

match code {
Code::InvalidArguments
| Code::SelectionTooLarge
| Code::RelationshipDepthExceeded
| Code::ResultBudgetExceeded
| Code::CapabilityStale
| Code::AuthorizationDenied
| Code::TemporarilyUnavailable
| Code::ToolUnavailable
| Code::ResolverValidationFailed
| Code::NotFound => code,
}
}

Expand Down
Loading