Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/tinyagents-definition/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ description = "Host-owned agent definition contract."
[dependencies]
async-trait = { workspace = true }
serde = { workspace = true }
# `AgentDefinition::tool_rules` carries the shared tool-rule vocabulary.
tinytools = { path = "../../vendor/tinytools/crates/tinytools", version = "0.5.0" }

[dev-dependencies]
serde_json = { workspace = true }
Expand Down
13 changes: 13 additions & 0 deletions crates/tinyagents-definition/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ pub struct AgentDefinition {
/// Canonical tool names this agent may use.
#[serde(default)]
pub tools: Vec<String>,
/// Pattern rules narrowing which tools this agent may see and call, on
/// top of [`Self::tools`]. Evaluated by the harness on the catalogue,
/// tool search and every call; see [`tinytools::ToolRules`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_rules: Option<tinytools::ToolRules>,
}

impl AgentDefinition {
Expand All @@ -81,6 +86,7 @@ impl AgentDefinition {
model: None,
subagents: Vec::new(),
tools: Vec::new(),
tool_rules: None,
}
}

Expand Down Expand Up @@ -113,6 +119,13 @@ impl AgentDefinition {
self
}

/// Sets the pattern rules narrowing this agent's tools.
#[must_use]
pub fn with_tool_rules(mut self, rules: tinytools::ToolRules) -> Self {
self.tool_rules = Some(rules);
self
}

/// Sets the host-defined routing role.
#[must_use]
pub fn with_role(mut self, role: impl Into<String>) -> Self {
Expand Down
3 changes: 3 additions & 0 deletions crates/tinyagents-harness/src/agent_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ mod terminal_outcome_test;
#[path = "mod_tests.rs"]
mod test;
#[cfg(test)]
#[path = "tool_rules_tests.rs"]
Comment thread
senamakel marked this conversation as resolved.
mod tool_rules_test;
#[cfg(test)]
#[path = "unknown_tool_tests.rs"]
mod unknown_tool_test;

Expand Down
23 changes: 17 additions & 6 deletions crates/tinyagents-harness/src/agent_loop/nested.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,17 +856,23 @@ impl<State: Send + Sync, Ctx: Send + Sync> AgentHarness<State, Ctx> {
mut call: ToolCall,
) -> Result<(Arc<dyn crate::tool::ToolDispatch<State, Ctx>>, ToolCall)> {
let name = call.name.clone();
let allowed_tools = self.resolve_tool_allowlist(ctx)?;
let is_allowed = allowed_tools
.as_ref()
.is_none_or(|allowed| allowed.contains(&name));
let Some(dispatch) = is_allowed
let gate = self.resolve_tool_gate(ctx)?;
let Some(dispatch) = gate
.allows_name(&name)
.then(|| self.tools.model_dispatch(&name))
.flatten()
else {
return Err(TinyAgentsError::ToolNotFound(name));
};
let tool = dispatch.tool();
// Tool rules apply to a nested call exactly as to a model call; a
// refusal reads like any other nested-call failure.
let rule_approval = match gate.admit_call(tool.as_ref(), &call.arguments) {
Comment thread
senamakel marked this conversation as resolved.
crate::tool::CallGate::Admit(approval) => approval,
crate::tool::CallGate::Refuse(message) => {
return Err(TinyAgentsError::ToolFailed(message));
}
};

// Same ordering rule as `admit_tool_call`: strip host-injected keys,
// inject the authoritative values, then validate the model-facing
Expand Down Expand Up @@ -911,7 +917,12 @@ impl<State: Send + Sync, Ctx: Send + Sync> AgentHarness<State, Ctx> {
))
})?;

if crate::tool::is_external_tool(tool.as_ref()) || tool.policy().access.approval_required {
let needs_approval = match rule_approval {
tinytools::ApprovalDirective::Required => true,
tinytools::ApprovalDirective::Waived => false,
tinytools::ApprovalDirective::Default => tool.policy().access.approval_required,
};
if crate::tool::is_external_tool(tool.as_ref()) || needs_approval {
return Err(approval_error(&name));
}

Expand Down
18 changes: 7 additions & 11 deletions crates/tinyagents-harness/src/agent_loop/run_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,12 @@ impl<State: Send + Sync, Ctx: Send + Sync> AgentHarness<State, Ctx> {

// Build the tool surface once (see `tool_surface.rs`): the direct tool
// set plus the deferred catalogue behind the `tool_search` bridge. The
// host allow-list gates both halves; `resolve_tool_allowlist` (not a raw
// read of `binding.allowed_tools`) is what applies I-9's fail-closed
// default, so an empty declared list denies every tool.
let allowed_tools = self.resolve_tool_allowlist(ctx)?;
let host_allows = |name: &str| {
allowed_tools
.as_ref()
.is_none_or(|allowed| allowed.contains(name))
};
let mut surface = self.build_tool_surface(ctx, messages, &host_allows).await?;
// tool gate (the host allow-list plus the run's tool rules) gates both
// halves; `resolve_tool_allowlist` underneath it (not a raw read of
// `binding.allowed_tools`) is what applies I-9's fail-closed default,
// so an empty declared list denies every tool.
let gate = self.resolve_tool_gate(ctx)?;
let mut surface = self.build_tool_surface(ctx, messages, &gate).await?;
self.check_structured_schema_name(&surface.tool_schemas)?;

status.mark_running(HarnessPhase::Middleware);
Expand Down Expand Up @@ -409,7 +405,7 @@ impl<State: Send + Sync, Ctx: Send + Sync> AgentHarness<State, Ctx> {
// the transcript it actually rewrote.
ctx.flush_transcript(self.policy.capture, messages);
let rewrote = surface
.declare_toolset_changes(self, ctx, messages, &host_allows, patch_profile.as_ref())
.declare_toolset_changes(self, ctx, messages, &gate, patch_profile.as_ref())
.await?;
let rewrote = surface.promote_discovered(messages, patch_profile.as_ref()) || rewrote;
if rewrote {
Expand Down
Loading
Loading