From 8554c57dba6ea5d89296ace23064cf4f8a5c546b Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 19 Sep 2026 00:05:45 +0300 Subject: [PATCH 1/2] Add trusted verbatim tool results Co-authored-by: Medulla --- README.md | 6 +++++ crates/tinytools/README.md | 7 ++++++ crates/tinytools/src/result/test.rs | 31 +++++++++++++++++++++++- crates/tinytools/src/result/types.rs | 35 +++++++++++++++++++++++++++- docs/specs/tinytools-vocabulary.md | 9 ++++++- 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 866171b..c51a22d 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,12 @@ rules are likewise wire contracts for persisted policy and registry introspection; literal-wire tests pin every timeout variant and a fully populated policy declaration. +`ToolResult::trusted_verbatim` is likewise persisted vocabulary. It defaults to +`false` and is omitted on serialization, preserving existing transcript and +RPC shapes. `ToolResult::verbatim()` records the exceptional `true` declaration +for a host that must preserve model-facing content byte-for-byte; it is data, +not a policy decision or an enforcement mechanism. + ## Injected arguments and call identity `ToolCall` / `ToolCallId` carry a model request's identity outside diff --git a/crates/tinytools/README.md b/crates/tinytools/README.md index 65f2754..25f4ba6 100644 --- a/crates/tinytools/README.md +++ b/crates/tinytools/README.md @@ -37,6 +37,13 @@ with a reported-error flag and optional markdown rendering. `ToolSpec` is the model-visible declaration. `ToolRunContext` exposes only tool-relevant run facts: workspace, thread id, and output cap. +`ToolResult::verbatim()` sets its serializable `trusted_verbatim` declaration. +The field defaults to `false` and is omitted from that ordinary wire shape. A +`true` value asks a consuming runtime to keep the model-facing content +byte-for-byte, for example an input schema, signature, or diff where a +faithful-looking rewrite would be incorrect. TinyTools only carries this data: +the host decides which producers are trusted and whether it honors the request. + `ToolPolicy` is the complete host-readable declaration around a call: - `ToolSideEffects` records filesystem, network, dependency, destructive, diff --git a/crates/tinytools/src/result/test.rs b/crates/tinytools/src/result/test.rs index 4ddd50e..c15e7f5 100644 --- a/crates/tinytools/src/result/test.rs +++ b/crates/tinytools/src/result/test.rs @@ -11,6 +11,7 @@ use super::{ToolContent, ToolResult}; fn success_carries_one_text_block() { let r = ToolResult::success("done"); assert!(!r.is_error); + assert!(!r.trusted_verbatim); assert_eq!(r.text(), "done"); assert_eq!(r.output(), "done"); } @@ -45,6 +46,7 @@ fn mixed_content_joins_in_order() { }, ], is_error: false, + trusted_verbatim: false, markdown_formatted: None, }; assert_eq!(r.text(), "line1\nline2"); @@ -59,6 +61,7 @@ fn empty_content_renders_empty() { let r = ToolResult { content: vec![], is_error: false, + trusted_verbatim: false, markdown_formatted: None, }; assert!(r.text().is_empty()); @@ -81,13 +84,14 @@ fn result_is_pinned_to_its_literal_wire_shape() { // deserializer that changed still agree with each other. Assert the exact // JSON a persisted transcript or RPC reply would carry, in both // directions. - let r = ToolResult::success_with_markdown(json!({"a": 1}), "**a**: 1"); + let r = ToolResult::success_with_markdown(json!({"a": 1}), "**a**: 1").verbatim(); let encoded: serde_json::Value = serde_json::to_value(&r).expect("serializable"); assert_eq!( encoded, json!({ "content": [{ "type": "json", "data": { "a": 1 } }], "is_error": false, + "trusted_verbatim": true, "markdownFormatted": "**a**: 1", }) ); @@ -95,6 +99,7 @@ fn result_is_pinned_to_its_literal_wire_shape() { let literal = r#"{"content":[{"type":"text","text":"hi"}],"is_error":true}"#; let decoded: ToolResult = serde_json::from_str(literal).expect("deserializable"); assert!(decoded.is_error); + assert!(!decoded.trusted_verbatim); assert_eq!(decoded.text(), "hi"); assert_eq!(decoded.markdown_formatted, None); } @@ -142,6 +147,15 @@ fn output_for_llm_falls_back_when_markdown_is_absent_or_blank() { assert_eq!(blank.output_for_llm(true), "plain"); } +#[test] +fn verbatim_builder_marks_the_result_without_changing_its_rendering() { + let result = ToolResult::success("--- a/file\n+++ b/file").verbatim(); + + assert!(result.trusted_verbatim); + assert_eq!(result.output(), "--- a/file\n+++ b/file"); + assert_eq!(result.output_for_llm(true), "--- a/file\n+++ b/file"); +} + #[test] fn the_markdown_field_keeps_its_composio_wire_name() { let r = ToolResult::success_with_markdown(json!({"a": 1}), "**a**: 1"); @@ -150,3 +164,18 @@ fn the_markdown_field_keeps_its_composio_wire_name() { let back: ToolResult = serde_json::from_str(&encoded).expect("deserializable"); assert_eq!(back.markdown_formatted.as_deref(), Some("**a**: 1")); } + +#[test] +fn trusted_verbatim_round_trips_and_false_is_omitted_from_the_wire() { + let trusted = ToolResult::success("schema").verbatim(); + let trusted_wire = serde_json::to_value(&trusted).expect("serializable"); + assert_eq!(trusted_wire["trusted_verbatim"], true); + let trusted_back: ToolResult = serde_json::from_value(trusted_wire).expect("deserializable"); + assert!(trusted_back.trusted_verbatim); + + let ordinary = ToolResult::success("ordinary"); + let ordinary_wire = serde_json::to_value(&ordinary).expect("serializable"); + assert!(ordinary_wire.get("trusted_verbatim").is_none()); + let ordinary_back: ToolResult = serde_json::from_value(ordinary_wire).expect("deserializable"); + assert!(!ordinary_back.trusted_verbatim); +} diff --git a/crates/tinytools/src/result/types.rs b/crates/tinytools/src/result/types.rs index e6cca0e..7a76b5d 100644 --- a/crates/tinytools/src/result/types.rs +++ b/crates/tinytools/src/result/types.rs @@ -2,7 +2,8 @@ use serde::{Deserialize, Serialize}; -/// Result of executing a tool: content blocks plus an error flag. +/// Result of executing a tool: content blocks plus reported-error and +/// delivery declarations. /// /// The block list is *conceptually* shaped like the Model Context Protocol's /// result — a list of content blocks plus a reported-error flag — which is what @@ -27,6 +28,16 @@ pub struct ToolResult { /// Indicates if the tool encountered an error during execution. #[serde(default)] pub is_error: bool, + /// Whether a consuming runtime should preserve the model-facing result + /// byte-for-byte. + /// + /// Defaults to `false`, the ordinary case where a host may safely batch, + /// frame, or compact output. `true` marks data for which a faithful-looking + /// rewrite is still wrong, such as an input schema, signature, or diff. + /// This is a declaration only: `TinyTools` does not decide which producers + /// may set it or require a host to honor it. + #[serde(default, skip_serializing_if = "is_false")] + pub trusted_verbatim: bool, /// Optional markdown rendering of the result. /// /// When the agent loop is configured with @@ -49,6 +60,7 @@ impl ToolResult { Self { content: vec![ToolContent::Text { text: text.into() }], is_error: false, + trusted_verbatim: false, markdown_formatted: None, } } @@ -63,6 +75,7 @@ impl ToolResult { text: message.into(), }], is_error: true, + trusted_verbatim: false, markdown_formatted: None, } } @@ -73,6 +86,7 @@ impl ToolResult { Self { content: vec![ToolContent::Json { data }], is_error: false, + trusted_verbatim: false, markdown_formatted: None, } } @@ -84,6 +98,7 @@ impl ToolResult { Self { content: vec![ToolContent::Json { data }], is_error: false, + trusted_verbatim: false, markdown_formatted: Some(markdown.into()), } } @@ -95,6 +110,19 @@ impl ToolResult { self } + /// Declares this result's model-facing content must be preserved unchanged. + /// + /// A host may ordinarily batch, frame, truncate, or compact tool output. + /// Use this opt-in only when such a transformation would make otherwise + /// plausible content incorrect, such as an input schema, signature, or + /// diff. This builder only carries the declaration; selecting trusted + /// producers and honoring the request remain host responsibilities. + #[must_use] + pub fn verbatim(mut self) -> Self { + self.trusted_verbatim = true; + self + } + /// The markdown rendering when present and non-blank, otherwise /// [`Self::output`]. /// @@ -143,6 +171,11 @@ impl ToolResult { } } +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_false(value: &bool) -> bool { + !*value +} + /// A single content block within a [`ToolResult`]. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] diff --git a/docs/specs/tinytools-vocabulary.md b/docs/specs/tinytools-vocabulary.md index b1bba1b..daf2841 100644 --- a/docs/specs/tinytools-vocabulary.md +++ b/docs/specs/tinytools-vocabulary.md @@ -20,7 +20,8 @@ error flag ends up inverted in one direction with nothing to catch it. and what it touches (privilege, scope, category, concurrency safety, external effect, timeout, result size cap, human-facing rendering). - Define `ToolResult` / `ToolContent`, the block-list result shape a tool - hands back, plus `ToolSpec`, the declaration a model is shown. + hands back, including an optional `trusted_verbatim` delivery declaration, + plus `ToolSpec`, the declaration a model is shown. - Define the permission ladder (`PermissionLevel`), the classification types (`ToolScope`, `ToolCategory`), and the per-invocation inputs that are not arguments (`ToolCallOptions`, `ToolTimeout`). @@ -126,6 +127,12 @@ section on the trait itself in `crates/tinytools/src/tool/types.rs`, and never supply an injected value: the preparation helper removes every declaration name before it reads a host value or derives the call id. Hosts validate only the prepared object, never the original model arguments. +- `ToolResult::trusted_verbatim` defaults to `false` and is omitted on that + ordinary wire shape. `ToolResult::verbatim()` records the exceptional `true` + declaration for model-facing schemas, signatures, diffs, and similar content + that a host should preserve byte-for-byte. The declaration carries no trust + enforcement: hosts decide whether a producer may set it and whether to honor + it. ## Acceptance criteria From 5a2127b42450fc9c83eeac2d9d7d263a7b1fc390 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 19 Sep 2026 01:18:40 +0300 Subject: [PATCH 2/2] fix: keep result trust at the host boundary Co-authored-by: Medulla --- README.md | 6 ----- crates/tinytools/README.md | 7 ------ crates/tinytools/src/result/test.rs | 31 +----------------------- crates/tinytools/src/result/types.rs | 35 +--------------------------- docs/specs/tinytools-vocabulary.md | 9 +------ 5 files changed, 3 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index c51a22d..866171b 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,6 @@ rules are likewise wire contracts for persisted policy and registry introspection; literal-wire tests pin every timeout variant and a fully populated policy declaration. -`ToolResult::trusted_verbatim` is likewise persisted vocabulary. It defaults to -`false` and is omitted on serialization, preserving existing transcript and -RPC shapes. `ToolResult::verbatim()` records the exceptional `true` declaration -for a host that must preserve model-facing content byte-for-byte; it is data, -not a policy decision or an enforcement mechanism. - ## Injected arguments and call identity `ToolCall` / `ToolCallId` carry a model request's identity outside diff --git a/crates/tinytools/README.md b/crates/tinytools/README.md index 25f4ba6..65f2754 100644 --- a/crates/tinytools/README.md +++ b/crates/tinytools/README.md @@ -37,13 +37,6 @@ with a reported-error flag and optional markdown rendering. `ToolSpec` is the model-visible declaration. `ToolRunContext` exposes only tool-relevant run facts: workspace, thread id, and output cap. -`ToolResult::verbatim()` sets its serializable `trusted_verbatim` declaration. -The field defaults to `false` and is omitted from that ordinary wire shape. A -`true` value asks a consuming runtime to keep the model-facing content -byte-for-byte, for example an input schema, signature, or diff where a -faithful-looking rewrite would be incorrect. TinyTools only carries this data: -the host decides which producers are trusted and whether it honors the request. - `ToolPolicy` is the complete host-readable declaration around a call: - `ToolSideEffects` records filesystem, network, dependency, destructive, diff --git a/crates/tinytools/src/result/test.rs b/crates/tinytools/src/result/test.rs index c15e7f5..4ddd50e 100644 --- a/crates/tinytools/src/result/test.rs +++ b/crates/tinytools/src/result/test.rs @@ -11,7 +11,6 @@ use super::{ToolContent, ToolResult}; fn success_carries_one_text_block() { let r = ToolResult::success("done"); assert!(!r.is_error); - assert!(!r.trusted_verbatim); assert_eq!(r.text(), "done"); assert_eq!(r.output(), "done"); } @@ -46,7 +45,6 @@ fn mixed_content_joins_in_order() { }, ], is_error: false, - trusted_verbatim: false, markdown_formatted: None, }; assert_eq!(r.text(), "line1\nline2"); @@ -61,7 +59,6 @@ fn empty_content_renders_empty() { let r = ToolResult { content: vec![], is_error: false, - trusted_verbatim: false, markdown_formatted: None, }; assert!(r.text().is_empty()); @@ -84,14 +81,13 @@ fn result_is_pinned_to_its_literal_wire_shape() { // deserializer that changed still agree with each other. Assert the exact // JSON a persisted transcript or RPC reply would carry, in both // directions. - let r = ToolResult::success_with_markdown(json!({"a": 1}), "**a**: 1").verbatim(); + let r = ToolResult::success_with_markdown(json!({"a": 1}), "**a**: 1"); let encoded: serde_json::Value = serde_json::to_value(&r).expect("serializable"); assert_eq!( encoded, json!({ "content": [{ "type": "json", "data": { "a": 1 } }], "is_error": false, - "trusted_verbatim": true, "markdownFormatted": "**a**: 1", }) ); @@ -99,7 +95,6 @@ fn result_is_pinned_to_its_literal_wire_shape() { let literal = r#"{"content":[{"type":"text","text":"hi"}],"is_error":true}"#; let decoded: ToolResult = serde_json::from_str(literal).expect("deserializable"); assert!(decoded.is_error); - assert!(!decoded.trusted_verbatim); assert_eq!(decoded.text(), "hi"); assert_eq!(decoded.markdown_formatted, None); } @@ -147,15 +142,6 @@ fn output_for_llm_falls_back_when_markdown_is_absent_or_blank() { assert_eq!(blank.output_for_llm(true), "plain"); } -#[test] -fn verbatim_builder_marks_the_result_without_changing_its_rendering() { - let result = ToolResult::success("--- a/file\n+++ b/file").verbatim(); - - assert!(result.trusted_verbatim); - assert_eq!(result.output(), "--- a/file\n+++ b/file"); - assert_eq!(result.output_for_llm(true), "--- a/file\n+++ b/file"); -} - #[test] fn the_markdown_field_keeps_its_composio_wire_name() { let r = ToolResult::success_with_markdown(json!({"a": 1}), "**a**: 1"); @@ -164,18 +150,3 @@ fn the_markdown_field_keeps_its_composio_wire_name() { let back: ToolResult = serde_json::from_str(&encoded).expect("deserializable"); assert_eq!(back.markdown_formatted.as_deref(), Some("**a**: 1")); } - -#[test] -fn trusted_verbatim_round_trips_and_false_is_omitted_from_the_wire() { - let trusted = ToolResult::success("schema").verbatim(); - let trusted_wire = serde_json::to_value(&trusted).expect("serializable"); - assert_eq!(trusted_wire["trusted_verbatim"], true); - let trusted_back: ToolResult = serde_json::from_value(trusted_wire).expect("deserializable"); - assert!(trusted_back.trusted_verbatim); - - let ordinary = ToolResult::success("ordinary"); - let ordinary_wire = serde_json::to_value(&ordinary).expect("serializable"); - assert!(ordinary_wire.get("trusted_verbatim").is_none()); - let ordinary_back: ToolResult = serde_json::from_value(ordinary_wire).expect("deserializable"); - assert!(!ordinary_back.trusted_verbatim); -} diff --git a/crates/tinytools/src/result/types.rs b/crates/tinytools/src/result/types.rs index 7a76b5d..e6cca0e 100644 --- a/crates/tinytools/src/result/types.rs +++ b/crates/tinytools/src/result/types.rs @@ -2,8 +2,7 @@ use serde::{Deserialize, Serialize}; -/// Result of executing a tool: content blocks plus reported-error and -/// delivery declarations. +/// Result of executing a tool: content blocks plus an error flag. /// /// The block list is *conceptually* shaped like the Model Context Protocol's /// result — a list of content blocks plus a reported-error flag — which is what @@ -28,16 +27,6 @@ pub struct ToolResult { /// Indicates if the tool encountered an error during execution. #[serde(default)] pub is_error: bool, - /// Whether a consuming runtime should preserve the model-facing result - /// byte-for-byte. - /// - /// Defaults to `false`, the ordinary case where a host may safely batch, - /// frame, or compact output. `true` marks data for which a faithful-looking - /// rewrite is still wrong, such as an input schema, signature, or diff. - /// This is a declaration only: `TinyTools` does not decide which producers - /// may set it or require a host to honor it. - #[serde(default, skip_serializing_if = "is_false")] - pub trusted_verbatim: bool, /// Optional markdown rendering of the result. /// /// When the agent loop is configured with @@ -60,7 +49,6 @@ impl ToolResult { Self { content: vec![ToolContent::Text { text: text.into() }], is_error: false, - trusted_verbatim: false, markdown_formatted: None, } } @@ -75,7 +63,6 @@ impl ToolResult { text: message.into(), }], is_error: true, - trusted_verbatim: false, markdown_formatted: None, } } @@ -86,7 +73,6 @@ impl ToolResult { Self { content: vec![ToolContent::Json { data }], is_error: false, - trusted_verbatim: false, markdown_formatted: None, } } @@ -98,7 +84,6 @@ impl ToolResult { Self { content: vec![ToolContent::Json { data }], is_error: false, - trusted_verbatim: false, markdown_formatted: Some(markdown.into()), } } @@ -110,19 +95,6 @@ impl ToolResult { self } - /// Declares this result's model-facing content must be preserved unchanged. - /// - /// A host may ordinarily batch, frame, truncate, or compact tool output. - /// Use this opt-in only when such a transformation would make otherwise - /// plausible content incorrect, such as an input schema, signature, or - /// diff. This builder only carries the declaration; selecting trusted - /// producers and honoring the request remain host responsibilities. - #[must_use] - pub fn verbatim(mut self) -> Self { - self.trusted_verbatim = true; - self - } - /// The markdown rendering when present and non-blank, otherwise /// [`Self::output`]. /// @@ -171,11 +143,6 @@ impl ToolResult { } } -#[allow(clippy::trivially_copy_pass_by_ref)] -fn is_false(value: &bool) -> bool { - !*value -} - /// A single content block within a [`ToolResult`]. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] diff --git a/docs/specs/tinytools-vocabulary.md b/docs/specs/tinytools-vocabulary.md index daf2841..b1bba1b 100644 --- a/docs/specs/tinytools-vocabulary.md +++ b/docs/specs/tinytools-vocabulary.md @@ -20,8 +20,7 @@ error flag ends up inverted in one direction with nothing to catch it. and what it touches (privilege, scope, category, concurrency safety, external effect, timeout, result size cap, human-facing rendering). - Define `ToolResult` / `ToolContent`, the block-list result shape a tool - hands back, including an optional `trusted_verbatim` delivery declaration, - plus `ToolSpec`, the declaration a model is shown. + hands back, plus `ToolSpec`, the declaration a model is shown. - Define the permission ladder (`PermissionLevel`), the classification types (`ToolScope`, `ToolCategory`), and the per-invocation inputs that are not arguments (`ToolCallOptions`, `ToolTimeout`). @@ -127,12 +126,6 @@ section on the trait itself in `crates/tinytools/src/tool/types.rs`, and never supply an injected value: the preparation helper removes every declaration name before it reads a host value or derives the call id. Hosts validate only the prepared object, never the original model arguments. -- `ToolResult::trusted_verbatim` defaults to `false` and is omitted on that - ordinary wire shape. `ToolResult::verbatim()` records the exceptional `true` - declaration for model-facing schemas, signatures, diffs, and similar content - that a host should preserve byte-for-byte. The declaration carries no trust - enforcement: hosts decide whether a producer may set it and whether to honor - it. ## Acceptance criteria