Skip to content

TinyTools

The vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call.

use tinytools::{Tool,ToolResult};structEcho;#[async_trait::async_trait]implToolforEcho{fnname(&self) -> &str{"echo"}fndescription(&self) -> &str{"Returns its input unchanged."}fnparameters_schema(&self) -> serde_json::Value{
serde_json::json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"],})}asyncfnexecute(&self,args: serde_json::Value) -> anyhow::Result<ToolResult>{let text = args.get("text").and_then(|v| v.as_str()).unwrap_or_default();Ok(ToolResult::success(text))}}

That is a complete tool. Everything else in the trait has a default.

Tool is async, so implementing it needs the async-trait shim above and beyond tinytools itself — this crate depends on it internally but does not re-export the macro. Add it as a direct dependency alongside tinytools:

[dependencies]
tinytools = "0.1"async-trait = "0.1"

Why this is its own crate

Two crates need these types and neither can own them. An agent harness has to name a tool's result to run a loop over it; a host application has to name the same result to implement one. When both declare their own, the conversions between them get written by hand at every seam — which is how an error flag ends up inverted in one direction with nothing to catch it.

So the vocabulary sits underneath both. A harness depends on this crate and re-exports it, so harness::ToolResult and tinytools::ToolResult are the same type, not structural twins. A tool author depends on this crate alone and compiles neither the harness nor the host.

What is here

ModuleHolds
toolTool — four required methods, and defaulted declarations describing what the tool needs and touches
resultToolResult, ToolContent — the MCP-shaped block list a tool hands back
specToolSpec — the declaration a model is shown
permissionPermissionLevel — the privilege ladder, ordered NoneDangerous
classificationToolScope, ToolCategory — where a tool may run, and which belt it is on
callToolCallOptions, ToolTimeout — per-invocation inputs that are not arguments
contextToolRunContext — the narrow seam onto a live run
workspaceWorkspaceDescriptor, SandboxMode — the root a tool may touch, and how strictly it is sandboxed
naminghumanize_tool_name, context_detail_from_args — rendering a call for a human

What is deliberately not here

No enforcement. Nothing in this crate checks a PermissionLevel, applies a ToolTimeout, or decides whether an external_effect needs approval. A tool describes itself and a host decides, because the decision depends on that host's threat model, its configuration, and who is asking — none of which generalize. Putting the check here would mean every host inherits one host's policy.

No registry, no dispatch, no execution loop. Those belong to whoever owns the run.

No dependency on an agent harness. The harness depends on this crate. ToolRunContext exists precisely so a tool can read run-scoped facts — the isolated-workspace root being the common one — without this crate naming the harness type that carries them. CI asserts the edge stays pointing one way.

The trait is a declaration, not an enforcement point

Beyond name / description / parameters_schema / execute, every method on Tool answers a question a host asks before it calls the tool: what privilege does this need, does it reach outside the machine, how long may it run, how should it read in a timeline. Most defaults are the cautious answer (scope is All, is_concurrency_safe is false, timeout_policy inherits the host's bound), but three fail open rather than closed and are what to check for when reviewing a Tool impl: external_effect defaults to false (an effectful tool that doesn't override it slips past a host's approval gate), max_result_size_chars defaults to None (no cap), and permission_level defaults to ReadOnly, not None, because most tools genuinely read.

Two consequences worth knowing:

  • A tool that exposes several actions should declare the minimum privilege any of them needs from permission_level, and the exact one from permission_level_with_args. Declaring the maximum statically blocks the tool for callers that could legitimately run its read-only half.
  • The argument-aware variants are the ones a host calls at the enforcement point. Overriding only external_effect on a tool whose classification depends on its arguments leaves the per-call case unhandled.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

Lint levels live in [workspace.lints] so local and CI runs agree. Library code may not unwrap, expect, or panic; test modules opt out at the top of the file.

License

GPL-3.0-only. See LICENSE.

About

Tool schema and agent friendly defaults

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - tinyhumansai/tinytools: Tool schema and agent friendly defaults · GitHub
Skip to content

TinyTools

The vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call.

use tinytools::{Tool,ToolResult};structEcho;#[async_trait::async_trait]implToolforEcho{fnname(&self) -> &str{"echo"}fndescription(&self) -> &str{"Returns its input unchanged."}fnparameters_schema(&self) -> serde_json::Value{
serde_json::json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"],})}asyncfnexecute(&self,args: serde_json::Value) -> anyhow::Result<ToolResult>{let text = args.get("text").and_then(|v| v.as_str()).unwrap_or_default();Ok(ToolResult::success(text))}}

That is a complete tool. Everything else in the trait has a default.

Tool is async, so implementing it needs the async-trait shim above and beyond tinytools itself — this crate depends on it internally but does not re-export the macro. Add it as a direct dependency alongside tinytools:

[dependencies]
tinytools = "0.1"async-trait = "0.1"

Why this is its own crate

Two crates need these types and neither can own them. An agent harness has to name a tool's result to run a loop over it; a host application has to name the same result to implement one. When both declare their own, the conversions between them get written by hand at every seam — which is how an error flag ends up inverted in one direction with nothing to catch it.

So the vocabulary sits underneath both. A harness depends on this crate and re-exports it, so harness::ToolResult and tinytools::ToolResult are the same type, not structural twins. A tool author depends on this crate alone and compiles neither the harness nor the host.

What is here

ModuleHolds
toolTool — four required methods, and defaulted declarations describing what the tool needs and touches
resultToolResult, ToolContent — the MCP-shaped block list a tool hands back
specToolSpec — the declaration a model is shown
permissionPermissionLevel — the privilege ladder, ordered NoneDangerous
classificationToolScope, ToolCategory — where a tool may run, and which belt it is on
callToolCallOptions, ToolTimeout — per-invocation inputs that are not arguments
contextToolRunContext — the narrow seam onto a live run
workspaceWorkspaceDescriptor, SandboxMode — the root a tool may touch, and how strictly it is sandboxed
naminghumanize_tool_name, context_detail_from_args — rendering a call for a human

What is deliberately not here

No enforcement. Nothing in this crate checks a PermissionLevel, applies a ToolTimeout, or decides whether an external_effect needs approval. A tool describes itself and a host decides, because the decision depends on that host's threat model, its configuration, and who is asking — none of which generalize. Putting the check here would mean every host inherits one host's policy.

No registry, no dispatch, no execution loop. Those belong to whoever owns the run.

No dependency on an agent harness. The harness depends on this crate. ToolRunContext exists precisely so a tool can read run-scoped facts — the isolated-workspace root being the common one — without this crate naming the harness type that carries them. CI asserts the edge stays pointing one way.

The trait is a declaration, not an enforcement point

Beyond name / description / parameters_schema / execute, every method on Tool answers a question a host asks before it calls the tool: what privilege does this need, does it reach outside the machine, how long may it run, how should it read in a timeline. Most defaults are the cautious answer (scope is All, is_concurrency_safe is false, timeout_policy inherits the host's bound), but three fail open rather than closed and are what to check for when reviewing a Tool impl: external_effect defaults to false (an effectful tool that doesn't override it slips past a host's approval gate), max_result_size_chars defaults to None (no cap), and permission_level defaults to ReadOnly, not None, because most tools genuinely read.

Two consequences worth knowing:

  • A tool that exposes several actions should declare the minimum privilege any of them needs from permission_level, and the exact one from permission_level_with_args. Declaring the maximum statically blocks the tool for callers that could legitimately run its read-only half.
  • The argument-aware variants are the ones a host calls at the enforcement point. Overriding only external_effect on a tool whose classification depends on its arguments leaves the per-call case unhandled.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

Lint levels live in [workspace.lints] so local and CI runs agree. Library code may not unwrap, expect, or panic; test modules opt out at the top of the file.

License

GPL-3.0-only. See LICENSE.

About

Tool schema and agent friendly defaults

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - tinyhumansai/tinytools: Tool schema and agent friendly defaults · GitHub
Skip to content

TinyTools

The vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call.

use tinytools::{Tool,ToolResult};structEcho;#[async_trait::async_trait]implToolforEcho{fnname(&self) -> &str{"echo"}fndescription(&self) -> &str{"Returns its input unchanged."}fnparameters_schema(&self) -> serde_json::Value{
serde_json::json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"],})}asyncfnexecute(&self,args: serde_json::Value) -> anyhow::Result<ToolResult>{let text = args.get("text").and_then(|v| v.as_str()).unwrap_or_default();Ok(ToolResult::success(text))}}

That is a complete tool. Everything else in the trait has a default.

Tool is async, so implementing it needs the async-trait shim above and beyond tinytools itself — this crate depends on it internally but does not re-export the macro. Add it as a direct dependency alongside tinytools:

[dependencies]
tinytools = "0.1"async-trait = "0.1"

Why this is its own crate

Two crates need these types and neither can own them. An agent harness has to name a tool's result to run a loop over it; a host application has to name the same result to implement one. When both declare their own, the conversions between them get written by hand at every seam — which is how an error flag ends up inverted in one direction with nothing to catch it.

So the vocabulary sits underneath both. A harness depends on this crate and re-exports it, so harness::ToolResult and tinytools::ToolResult are the same type, not structural twins. A tool author depends on this crate alone and compiles neither the harness nor the host.

What is here

ModuleHolds
toolTool — four required methods, and defaulted declarations describing what the tool needs and touches
resultToolResult, ToolContent — the MCP-shaped block list a tool hands back
specToolSpec — the declaration a model is shown
permissionPermissionLevel — the privilege ladder, ordered NoneDangerous
classificationToolScope, ToolCategory — where a tool may run, and which belt it is on
callToolCallOptions, ToolTimeout — per-invocation inputs that are not arguments
contextToolRunContext — the narrow seam onto a live run
workspaceWorkspaceDescriptor, SandboxMode — the root a tool may touch, and how strictly it is sandboxed
naminghumanize_tool_name, context_detail_from_args — rendering a call for a human

What is deliberately not here

No enforcement. Nothing in this crate checks a PermissionLevel, applies a ToolTimeout, or decides whether an external_effect needs approval. A tool describes itself and a host decides, because the decision depends on that host's threat model, its configuration, and who is asking — none of which generalize. Putting the check here would mean every host inherits one host's policy.

No registry, no dispatch, no execution loop. Those belong to whoever owns the run.

No dependency on an agent harness. The harness depends on this crate. ToolRunContext exists precisely so a tool can read run-scoped facts — the isolated-workspace root being the common one — without this crate naming the harness type that carries them. CI asserts the edge stays pointing one way.

The trait is a declaration, not an enforcement point

Beyond name / description / parameters_schema / execute, every method on Tool answers a question a host asks before it calls the tool: what privilege does this need, does it reach outside the machine, how long may it run, how should it read in a timeline. Most defaults are the cautious answer (scope is All, is_concurrency_safe is false, timeout_policy inherits the host's bound), but three fail open rather than closed and are what to check for when reviewing a Tool impl: external_effect defaults to false (an effectful tool that doesn't override it slips past a host's approval gate), max_result_size_chars defaults to None (no cap), and permission_level defaults to ReadOnly, not None, because most tools genuinely read.

Two consequences worth knowing:

  • A tool that exposes several actions should declare the minimum privilege any of them needs from permission_level, and the exact one from permission_level_with_args. Declaring the maximum statically blocks the tool for callers that could legitimately run its read-only half.
  • The argument-aware variants are the ones a host calls at the enforcement point. Overriding only external_effect on a tool whose classification depends on its arguments leaves the per-call case unhandled.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

Lint levels live in [workspace.lints] so local and CI runs agree. Library code may not unwrap, expect, or panic; test modules opt out at the top of the file.

License

GPL-3.0-only. See LICENSE.

About

Tool schema and agent friendly defaults

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - tinyhumansai/tinytools: Tool schema and agent friendly defaults · GitHub
Skip to content

TinyTools

The vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call.

use tinytools::{Tool,ToolResult};structEcho;#[async_trait::async_trait]implToolforEcho{fnname(&self) -> &str{"echo"}fndescription(&self) -> &str{"Returns its input unchanged."}fnparameters_schema(&self) -> serde_json::Value{
serde_json::json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"],})}asyncfnexecute(&self,args: serde_json::Value) -> anyhow::Result<ToolResult>{let text = args.get("text").and_then(|v| v.as_str()).unwrap_or_default();Ok(ToolResult::success(text))}}

That is a complete tool. Everything else in the trait has a default.

Tool is async, so implementing it needs the async-trait shim above and beyond tinytools itself — this crate depends on it internally but does not re-export the macro. Add it as a direct dependency alongside tinytools:

[dependencies]
tinytools = "0.1"async-trait = "0.1"

Why this is its own crate

Two crates need these types and neither can own them. An agent harness has to name a tool's result to run a loop over it; a host application has to name the same result to implement one. When both declare their own, the conversions between them get written by hand at every seam — which is how an error flag ends up inverted in one direction with nothing to catch it.

So the vocabulary sits underneath both. A harness depends on this crate and re-exports it, so harness::ToolResult and tinytools::ToolResult are the same type, not structural twins. A tool author depends on this crate alone and compiles neither the harness nor the host.

What is here

ModuleHolds
toolTool — four required methods, and defaulted declarations describing what the tool needs and touches
resultToolResult, ToolContent — the MCP-shaped block list a tool hands back
specToolSpec — the declaration a model is shown
permissionPermissionLevel — the privilege ladder, ordered NoneDangerous
classificationToolScope, ToolCategory — where a tool may run, and which belt it is on
callToolCallOptions, ToolTimeout — per-invocation inputs that are not arguments
contextToolRunContext — the narrow seam onto a live run
workspaceWorkspaceDescriptor, SandboxMode — the root a tool may touch, and how strictly it is sandboxed
naminghumanize_tool_name, context_detail_from_args — rendering a call for a human

What is deliberately not here

No enforcement. Nothing in this crate checks a PermissionLevel, applies a ToolTimeout, or decides whether an external_effect needs approval. A tool describes itself and a host decides, because the decision depends on that host's threat model, its configuration, and who is asking — none of which generalize. Putting the check here would mean every host inherits one host's policy.

No registry, no dispatch, no execution loop. Those belong to whoever owns the run.

No dependency on an agent harness. The harness depends on this crate. ToolRunContext exists precisely so a tool can read run-scoped facts — the isolated-workspace root being the common one — without this crate naming the harness type that carries them. CI asserts the edge stays pointing one way.

The trait is a declaration, not an enforcement point

Beyond name / description / parameters_schema / execute, every method on Tool answers a question a host asks before it calls the tool: what privilege does this need, does it reach outside the machine, how long may it run, how should it read in a timeline. Most defaults are the cautious answer (scope is All, is_concurrency_safe is false, timeout_policy inherits the host's bound), but three fail open rather than closed and are what to check for when reviewing a Tool impl: external_effect defaults to false (an effectful tool that doesn't override it slips past a host's approval gate), max_result_size_chars defaults to None (no cap), and permission_level defaults to ReadOnly, not None, because most tools genuinely read.

Two consequences worth knowing:

  • A tool that exposes several actions should declare the minimum privilege any of them needs from permission_level, and the exact one from permission_level_with_args. Declaring the maximum statically blocks the tool for callers that could legitimately run its read-only half.
  • The argument-aware variants are the ones a host calls at the enforcement point. Overriding only external_effect on a tool whose classification depends on its arguments leaves the per-call case unhandled.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

Lint levels live in [workspace.lints] so local and CI runs agree. Library code may not unwrap, expect, or panic; test modules opt out at the top of the file.

License

GPL-3.0-only. See LICENSE.

About

Tool schema and agent friendly defaults

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - tinyhumansai/tinytools: Tool schema and agent friendly defaults · GitHub
Skip to content

TinyTools

The vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call.

use tinytools::{Tool,ToolResult};structEcho;#[async_trait::async_trait]implToolforEcho{fnname(&self) -> &str{"echo"}fndescription(&self) -> &str{"Returns its input unchanged."}fnparameters_schema(&self) -> serde_json::Value{
serde_json::json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"],})}asyncfnexecute(&self,args: serde_json::Value) -> anyhow::Result<ToolResult>{let text = args.get("text").and_then(|v| v.as_str()).unwrap_or_default();Ok(ToolResult::success(text))}}

That is a complete tool. Everything else in the trait has a default.

Tool is async, so implementing it needs the async-trait shim above and beyond tinytools itself — this crate depends on it internally but does not re-export the macro. Add it as a direct dependency alongside tinytools:

[dependencies]
tinytools = "0.1"async-trait = "0.1"

Why this is its own crate

Two crates need these types and neither can own them. An agent harness has to name a tool's result to run a loop over it; a host application has to name the same result to implement one. When both declare their own, the conversions between them get written by hand at every seam — which is how an error flag ends up inverted in one direction with nothing to catch it.

So the vocabulary sits underneath both. A harness depends on this crate and re-exports it, so harness::ToolResult and tinytools::ToolResult are the same type, not structural twins. A tool author depends on this crate alone and compiles neither the harness nor the host.

What is here

ModuleHolds
toolTool — four required methods, and defaulted declarations describing what the tool needs and touches
resultToolResult, ToolContent — the MCP-shaped block list a tool hands back
specToolSpec — the declaration a model is shown
permissionPermissionLevel — the privilege ladder, ordered NoneDangerous
classificationToolScope, ToolCategory — where a tool may run, and which belt it is on
callToolCallOptions, ToolTimeout — per-invocation inputs that are not arguments
contextToolRunContext — the narrow seam onto a live run
workspaceWorkspaceDescriptor, SandboxMode — the root a tool may touch, and how strictly it is sandboxed
naminghumanize_tool_name, context_detail_from_args — rendering a call for a human

What is deliberately not here

No enforcement. Nothing in this crate checks a PermissionLevel, applies a ToolTimeout, or decides whether an external_effect needs approval. A tool describes itself and a host decides, because the decision depends on that host's threat model, its configuration, and who is asking — none of which generalize. Putting the check here would mean every host inherits one host's policy.

No registry, no dispatch, no execution loop. Those belong to whoever owns the run.

No dependency on an agent harness. The harness depends on this crate. ToolRunContext exists precisely so a tool can read run-scoped facts — the isolated-workspace root being the common one — without this crate naming the harness type that carries them. CI asserts the edge stays pointing one way.

The trait is a declaration, not an enforcement point

Beyond name / description / parameters_schema / execute, every method on Tool answers a question a host asks before it calls the tool: what privilege does this need, does it reach outside the machine, how long may it run, how should it read in a timeline. Most defaults are the cautious answer (scope is All, is_concurrency_safe is false, timeout_policy inherits the host's bound), but three fail open rather than closed and are what to check for when reviewing a Tool impl: external_effect defaults to false (an effectful tool that doesn't override it slips past a host's approval gate), max_result_size_chars defaults to None (no cap), and permission_level defaults to ReadOnly, not None, because most tools genuinely read.

Two consequences worth knowing:

  • A tool that exposes several actions should declare the minimum privilege any of them needs from permission_level, and the exact one from permission_level_with_args. Declaring the maximum statically blocks the tool for callers that could legitimately run its read-only half.
  • The argument-aware variants are the ones a host calls at the enforcement point. Overriding only external_effect on a tool whose classification depends on its arguments leaves the per-call case unhandled.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

Lint levels live in [workspace.lints] so local and CI runs agree. Library code may not unwrap, expect, or panic; test modules opt out at the top of the file.

License

GPL-3.0-only. See LICENSE.

About

Tool schema and agent friendly defaults

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - tinyhumansai/tinytools: Tool schema and agent friendly defaults · GitHub
Skip to content

TinyTools

The vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call.

use tinytools::{Tool,ToolResult};structEcho;#[async_trait::async_trait]implToolforEcho{fnname(&self) -> &str{"echo"}fndescription(&self) -> &str{"Returns its input unchanged."}fnparameters_schema(&self) -> serde_json::Value{
serde_json::json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"],})}asyncfnexecute(&self,args: serde_json::Value) -> anyhow::Result<ToolResult>{let text = args.get("text").and_then(|v| v.as_str()).unwrap_or_default();Ok(ToolResult::success(text))}}

That is a complete tool. Everything else in the trait has a default.

Tool is async, so implementing it needs the async-trait shim above and beyond tinytools itself — this crate depends on it internally but does not re-export the macro. Add it as a direct dependency alongside tinytools:

[dependencies]
tinytools = "0.1"async-trait = "0.1"

Why this is its own crate

Two crates need these types and neither can own them. An agent harness has to name a tool's result to run a loop over it; a host application has to name the same result to implement one. When both declare their own, the conversions between them get written by hand at every seam — which is how an error flag ends up inverted in one direction with nothing to catch it.

So the vocabulary sits underneath both. A harness depends on this crate and re-exports it, so harness::ToolResult and tinytools::ToolResult are the same type, not structural twins. A tool author depends on this crate alone and compiles neither the harness nor the host.

What is here

ModuleHolds
toolTool — four required methods, and defaulted declarations describing what the tool needs and touches
resultToolResult, ToolContent — the MCP-shaped block list a tool hands back
specToolSpec — the declaration a model is shown
permissionPermissionLevel — the privilege ladder, ordered NoneDangerous
classificationToolScope, ToolCategory — where a tool may run, and which belt it is on
callToolCallOptions, ToolTimeout — per-invocation inputs that are not arguments
contextToolRunContext — the narrow seam onto a live run
workspaceWorkspaceDescriptor, SandboxMode — the root a tool may touch, and how strictly it is sandboxed
naminghumanize_tool_name, context_detail_from_args — rendering a call for a human

What is deliberately not here

No enforcement. Nothing in this crate checks a PermissionLevel, applies a ToolTimeout, or decides whether an external_effect needs approval. A tool describes itself and a host decides, because the decision depends on that host's threat model, its configuration, and who is asking — none of which generalize. Putting the check here would mean every host inherits one host's policy.

No registry, no dispatch, no execution loop. Those belong to whoever owns the run.

No dependency on an agent harness. The harness depends on this crate. ToolRunContext exists precisely so a tool can read run-scoped facts — the isolated-workspace root being the common one — without this crate naming the harness type that carries them. CI asserts the edge stays pointing one way.

The trait is a declaration, not an enforcement point

Beyond name / description / parameters_schema / execute, every method on Tool answers a question a host asks before it calls the tool: what privilege does this need, does it reach outside the machine, how long may it run, how should it read in a timeline. Most defaults are the cautious answer (scope is All, is_concurrency_safe is false, timeout_policy inherits the host's bound), but three fail open rather than closed and are what to check for when reviewing a Tool impl: external_effect defaults to false (an effectful tool that doesn't override it slips past a host's approval gate), max_result_size_chars defaults to None (no cap), and permission_level defaults to ReadOnly, not None, because most tools genuinely read.

Two consequences worth knowing:

  • A tool that exposes several actions should declare the minimum privilege any of them needs from permission_level, and the exact one from permission_level_with_args. Declaring the maximum statically blocks the tool for callers that could legitimately run its read-only half.
  • The argument-aware variants are the ones a host calls at the enforcement point. Overriding only external_effect on a tool whose classification depends on its arguments leaves the per-call case unhandled.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

Lint levels live in [workspace.lints] so local and CI runs agree. Library code may not unwrap, expect, or panic; test modules opt out at the top of the file.

License

GPL-3.0-only. See LICENSE.

About

Tool schema and agent friendly defaults

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - tinyhumansai/tinytools: Tool schema and agent friendly defaults · GitHub
Skip to content

TinyTools

The vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call.

use tinytools::{Tool,ToolResult};structEcho;#[async_trait::async_trait]implToolforEcho{fnname(&self) -> &str{"echo"}fndescription(&self) -> &str{"Returns its input unchanged."}fnparameters_schema(&self) -> serde_json::Value{
serde_json::json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"],})}asyncfnexecute(&self,args: serde_json::Value) -> anyhow::Result<ToolResult>{let text = args.get("text").and_then(|v| v.as_str()).unwrap_or_default();Ok(ToolResult::success(text))}}

That is a complete tool. Everything else in the trait has a default.

Tool is async, so implementing it needs the async-trait shim above and beyond tinytools itself — this crate depends on it internally but does not re-export the macro. Add it as a direct dependency alongside tinytools:

[dependencies]
tinytools = "0.1"async-trait = "0.1"

Why this is its own crate

Two crates need these types and neither can own them. An agent harness has to name a tool's result to run a loop over it; a host application has to name the same result to implement one. When both declare their own, the conversions between them get written by hand at every seam — which is how an error flag ends up inverted in one direction with nothing to catch it.

So the vocabulary sits underneath both. A harness depends on this crate and re-exports it, so harness::ToolResult and tinytools::ToolResult are the same type, not structural twins. A tool author depends on this crate alone and compiles neither the harness nor the host.

What is here

ModuleHolds
toolTool — four required methods, and defaulted declarations describing what the tool needs and touches
resultToolResult, ToolContent — the MCP-shaped block list a tool hands back
specToolSpec — the declaration a model is shown
permissionPermissionLevel — the privilege ladder, ordered NoneDangerous
classificationToolScope, ToolCategory — where a tool may run, and which belt it is on
callToolCallOptions, ToolTimeout — per-invocation inputs that are not arguments
contextToolRunContext — the narrow seam onto a live run
workspaceWorkspaceDescriptor, SandboxMode — the root a tool may touch, and how strictly it is sandboxed
naminghumanize_tool_name, context_detail_from_args — rendering a call for a human

What is deliberately not here

No enforcement. Nothing in this crate checks a PermissionLevel, applies a ToolTimeout, or decides whether an external_effect needs approval. A tool describes itself and a host decides, because the decision depends on that host's threat model, its configuration, and who is asking — none of which generalize. Putting the check here would mean every host inherits one host's policy.

No registry, no dispatch, no execution loop. Those belong to whoever owns the run.

No dependency on an agent harness. The harness depends on this crate. ToolRunContext exists precisely so a tool can read run-scoped facts — the isolated-workspace root being the common one — without this crate naming the harness type that carries them. CI asserts the edge stays pointing one way.

The trait is a declaration, not an enforcement point

Beyond name / description / parameters_schema / execute, every method on Tool answers a question a host asks before it calls the tool: what privilege does this need, does it reach outside the machine, how long may it run, how should it read in a timeline. Most defaults are the cautious answer (scope is All, is_concurrency_safe is false, timeout_policy inherits the host's bound), but three fail open rather than closed and are what to check for when reviewing a Tool impl: external_effect defaults to false (an effectful tool that doesn't override it slips past a host's approval gate), max_result_size_chars defaults to None (no cap), and permission_level defaults to ReadOnly, not None, because most tools genuinely read.

Two consequences worth knowing:

  • A tool that exposes several actions should declare the minimum privilege any of them needs from permission_level, and the exact one from permission_level_with_args. Declaring the maximum statically blocks the tool for callers that could legitimately run its read-only half.
  • The argument-aware variants are the ones a host calls at the enforcement point. Overriding only external_effect on a tool whose classification depends on its arguments leaves the per-call case unhandled.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

Lint levels live in [workspace.lints] so local and CI runs agree. Library code may not unwrap, expect, or panic; test modules opt out at the top of the file.

License

GPL-3.0-only. See LICENSE.

About

Tool schema and agent friendly defaults

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - tinyhumansai/tinytools: Tool schema and agent friendly defaults · GitHub
Skip to content

TinyTools

The vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call.

use tinytools::{Tool,ToolResult};structEcho;#[async_trait::async_trait]implToolforEcho{fnname(&self) -> &str{"echo"}fndescription(&self) -> &str{"Returns its input unchanged."}fnparameters_schema(&self) -> serde_json::Value{
serde_json::json!({"type":"object","properties":{"text":{"type":"string"}},"required":["text"],})}asyncfnexecute(&self,args: serde_json::Value) -> anyhow::Result<ToolResult>{let text = args.get("text").and_then(|v| v.as_str()).unwrap_or_default();Ok(ToolResult::success(text))}}

That is a complete tool. Everything else in the trait has a default.

Tool is async, so implementing it needs the async-trait shim above and beyond tinytools itself — this crate depends on it internally but does not re-export the macro. Add it as a direct dependency alongside tinytools:

[dependencies]
tinytools = "0.1"async-trait = "0.1"

Why this is its own crate

Two crates need these types and neither can own them. An agent harness has to name a tool's result to run a loop over it; a host application has to name the same result to implement one. When both declare their own, the conversions between them get written by hand at every seam — which is how an error flag ends up inverted in one direction with nothing to catch it.

So the vocabulary sits underneath both. A harness depends on this crate and re-exports it, so harness::ToolResult and tinytools::ToolResult are the same type, not structural twins. A tool author depends on this crate alone and compiles neither the harness nor the host.

What is here

ModuleHolds
toolTool — four required methods, and defaulted declarations describing what the tool needs and touches
resultToolResult, ToolContent — the MCP-shaped block list a tool hands back
specToolSpec — the declaration a model is shown
permissionPermissionLevel — the privilege ladder, ordered NoneDangerous
classificationToolScope, ToolCategory — where a tool may run, and which belt it is on
callToolCallOptions, ToolTimeout — per-invocation inputs that are not arguments
contextToolRunContext — the narrow seam onto a live run
workspaceWorkspaceDescriptor, SandboxMode — the root a tool may touch, and how strictly it is sandboxed
naminghumanize_tool_name, context_detail_from_args — rendering a call for a human

What is deliberately not here

No enforcement. Nothing in this crate checks a PermissionLevel, applies a ToolTimeout, or decides whether an external_effect needs approval. A tool describes itself and a host decides, because the decision depends on that host's threat model, its configuration, and who is asking — none of which generalize. Putting the check here would mean every host inherits one host's policy.

No registry, no dispatch, no execution loop. Those belong to whoever owns the run.

No dependency on an agent harness. The harness depends on this crate. ToolRunContext exists precisely so a tool can read run-scoped facts — the isolated-workspace root being the common one — without this crate naming the harness type that carries them. CI asserts the edge stays pointing one way.

The trait is a declaration, not an enforcement point

Beyond name / description / parameters_schema / execute, every method on Tool answers a question a host asks before it calls the tool: what privilege does this need, does it reach outside the machine, how long may it run, how should it read in a timeline. Most defaults are the cautious answer (scope is All, is_concurrency_safe is false, timeout_policy inherits the host's bound), but three fail open rather than closed and are what to check for when reviewing a Tool impl: external_effect defaults to false (an effectful tool that doesn't override it slips past a host's approval gate), max_result_size_chars defaults to None (no cap), and permission_level defaults to ReadOnly, not None, because most tools genuinely read.

Two consequences worth knowing:

  • A tool that exposes several actions should declare the minimum privilege any of them needs from permission_level, and the exact one from permission_level_with_args. Declaring the maximum statically blocks the tool for callers that could legitimately run its read-only half.
  • The argument-aware variants are the ones a host calls at the enforcement point. Overriding only external_effect on a tool whose classification depends on its arguments leaves the per-call case unhandled.

Development

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

Lint levels live in [workspace.lints] so local and CI runs agree. Library code may not unwrap, expect, or panic; test modules opt out at the top of the file.

License

GPL-3.0-only. See LICENSE.

About

Tool schema and agent friendly defaults

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages